package com.erp.crosstenant;

import com.erp.tenant.DbContextHolder;
import com.erp.tenant.TenantDataSourceFactory;
import com.erp.tenant.TenantDbSettings;
import com.erp.tenant.TenantRoutingDataSource;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * SUPER_ADMIN 의 어드민 14개 메뉴 전사 합산용 fan-out 집계기.
 *
 * 호출자(컨트롤러)는 {@link #fanOut(String, Map)} 에 cross-tenant 전용 mapper id 와
 * 파라미터를 넘긴다. Aggregator 가 메타 DB 에서 활성 회사 목록을 가져온 뒤,
 * 회사마다 {@link DbContextHolder} 를 잠깐 그 회사 DB 로 바꿔 같은 SELECT 를 돌리고,
 * 모든 응답 행에 {@code company_code} 를 박아 머지한다.
 *
 * 핵심 원칙 (설계서 §3, §5.3):
 *   - 한 회사 실패해도 전체는 진행 (fail-open). 실패 카운트만 누적.
 *   - 모든 행에 {@code company_code} 추가 (응답 측에서 회사 필터/그룹 가능하도록).
 *   - SELECT 만. UPDATE/DELETE 는 회사 도메인 컨텍스트로 위임.
 *   - 풀 정책 무변경 — 회사 풀 {@code minIdle=0} 그대로 유지.
 *
 * 1차 구현은 직렬·캐시 OFF (설계서 §7.2 — N≤20 까진 충분).
 *
 * @see com.erp.crosstenant.CrossTenantContext
 * @see com.erp.tenant.TenantRoutingDataSource
 * @see com.erp.provisioning.CompanyStatsService  // raw JDBC 1세대 패턴 (참고용)
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class CrossTenantAggregator {

    private final SqlSession sqlSession;
    private final TenantRoutingDataSource routingDataSource;
    private final TenantDbSettings tenantDbSettings;

    /** Aggregator 결과 봉투. 컨트롤러는 이걸 그대로 ApiResponse.data 로 감싸 반환. */
    public static class Result {
        public final List<Map<String, Object>> rows;
        public final int companies_queried;
        public final int companies_failed;
        public final List<String> failed_company_codes;
        public final List<String> truncated_company_codes;

        public Result(List<Map<String, Object>> rows,
                      int companies_queried,
                      int companies_failed,
                      List<String> failed_company_codes,
                      List<String> truncated_company_codes) {
            this.rows = rows;
            this.companies_queried = companies_queried;
            this.companies_failed = companies_failed;
            this.failed_company_codes = failed_company_codes;
            this.truncated_company_codes = truncated_company_codes;
        }

        public List<Map<String, Object>> getRows() { return rows; }
        public int getCompanies_queried() { return companies_queried; }
        public int getCompanies_failed() { return companies_failed; }
        public List<String> getFailed_company_codes() { return failed_company_codes; }
        public List<String> getTruncated_company_codes() { return truncated_company_codes; }
    }

    /**
     * 활성 회사 N 개에 fan-out (cap 없음, 메타 포함 X).
     */
    public Result fanOut(String mapperId, Map<String, Object> params) {
        return fanOut(mapperId, params, null, false);
    }

    /**
     * 활성 회사 N 개에 fan-out + 회사당 cap (메타 포함 X).
     */
    public Result fanOut(String mapperId, Map<String, Object> params, Integer perCompanyLimit) {
        return fanOut(mapperId, params, perCompanyLimit, false);
    }

    /**
     * 활성 회사 N 개에 fan-out + 회사당 cap + 옵션으로 메타 DB 도 같이 조회.
     * 호출자는 미리 {@link CrossTenantContext#requireSuperAdmin}, {@link CrossTenantContext#requireMetaContext}
     * 통과를 확인해야 한다 (Aggregator 자체는 권한 가드 안 함 — 단일 책임).
     *
     * cap 동작:
     *   - mapper 가 {@code LIMIT #{per_company_limit_plus_one}} 으로 cap+1 만큼 가져옴.
     *   - Aggregator 가 회사별로 rows.size() > cap 인지 검사 → 초과면 잘라 cap 까지만 반환 + truncated 마킹.
     *
     * 메타 포함 (includeMeta=true):
     *   - 회사 fan-out 전에 메타 DB 에서 같은 mapper 한 번 더 실행.
     *   - 메타 행에는 {@code company_code='*'} 박힘 (시스템/공통 사용자 표시 — 기존 회사관리 화면 컨벤션과 동일).
     *   - 사용자관리처럼 "SUPER_ADMIN 도 관리 대상"인 도메인에서만 사용. roles/batches 등은 메타에 의미있는 데이터 없으니 false.
     *
     * @param mapperId          cross-tenant 전용 mapper id
     * @param params            SQL 바인딩 파라미터
     * @param perCompanyLimit   회사당 cap. null 이면 cap 없음
     * @param includeMeta       true 면 메타 DB 도 한 번 조회해서 결과 앞에 prepend (company_code='*')
     * @return fan-out 결과
     */
    public Result fanOut(String mapperId, Map<String, Object> params, Integer perCompanyLimit, boolean includeMeta) {
        // 호출 직전 컨텍스트는 META 여야 한다 (CrossTenantContext.requireMetaContext 통과 후라고 가정).
        List<Map<String, Object>> companies = sqlSession.selectList("provisioning.listActiveCompanies");

        List<Map<String, Object>> mergedRows = new ArrayList<>();
        List<String> failed = new ArrayList<>();
        List<String> truncated = new ArrayList<>();
        int queried = 0;

        // includeMeta=true 면 메타 DB 결과를 먼저 추가 (company_code='*')
        // — 회사 fan-out 들어가기 전에 META 컨텍스트 그대로에서 실행.
        if (includeMeta) {
            try {
                List<Map<String, Object>> metaRows = params == null
                        ? sqlSession.selectList(mapperId)
                        : sqlSession.selectList(mapperId, params);
                if (perCompanyLimit != null && metaRows.size() > perCompanyLimit) {
                    metaRows = new ArrayList<>(metaRows.subList(0, perCompanyLimit));
                    truncated.add("*");
                }
                for (Map<String, Object> r : metaRows) {
                    r.put("company_code", "*");
                    mergedRows.add(r);
                }
            } catch (Exception e) {
                log.warn("[CrossTenant] meta query mapper={} failed: {}", mapperId, e.getMessage());
                failed.add("*");
            }
        }

        for (Map<String, Object> c : companies) {
            String companyCode = (String) c.get("company_code");
            String dbName = (String) c.get("db_name");
            queried++;

            try {
                List<Map<String, Object>> rows = queryOne(dbName, mapperId, params);

                // cap 적용 — mapper 가 cap+1 만큼 가져왔으면 cap 으로 잘라 truncated 마킹
                if (perCompanyLimit != null && rows.size() > perCompanyLimit) {
                    rows = new ArrayList<>(rows.subList(0, perCompanyLimit));
                    truncated.add(companyCode);
                }

                for (Map<String, Object> r : rows) {
                    // company_code 강제 주입 — 응답 행이 어느 회사 것인지 명시.
                    // mapper 내부에서 박은 값이 있더라도 메타 DB 라우팅 정보로 덮어씀.
                    r.put("company_code", companyCode);
                    mergedRows.add(r);
                }
            } catch (Exception e) {
                log.warn("[CrossTenant] mapper={} failed for company={} db={} : {}",
                        mapperId, companyCode, dbName, e.getMessage());
                failed.add(companyCode);
            }
        }

        return new Result(mergedRows, queried, failed.size(), failed, truncated);
    }

    /**
     * 한 회사 DB 로 컨텍스트 잠깐 바꿔 SELECT 1번.
     * finally 에서 반드시 prev 복원 — 누수되면 후속 요청이 엉뚱한 회사 DB 로 라우팅됨.
     *
     * ★ 핵심: TenantRoutingDataSource 의 routing map 에 회사 DB 풀이 등록돼있지 않으면
     * META 로 fallback 됨. SubdomainResolverFilter 는 회사 도메인 진입 시에만 풀을 등록하므로,
     * SUPER_ADMIN 이 admin 도메인에서 호출하는 cross-tenant 경로에선 풀이 안 깔려 있다.
     * 따라서 매 호출마다 ensureTenantPool 로 lazy 생성 (이미 있으면 no-op).
     */
    private List<Map<String, Object>> queryOne(String dbName, String mapperId, Map<String, Object> params) {
        ensureTenantPool(dbName);
        String prev = DbContextHolder.get();
        try {
            DbContextHolder.set(dbName);
            return params == null
                    ? sqlSession.selectList(mapperId)
                    : sqlSession.selectList(mapperId, params);
        } finally {
            if (prev == null) {
                DbContextHolder.clear();
            } else {
                DbContextHolder.set(prev);
            }
        }
    }

    /**
     * 회사 DB 풀이 없으면 최초 1회 생성. minIdle=0 정책은 Factory 가 책임.
     * SubdomainResolverFilter.ensureTenantPool 과 동일한 패턴.
     */
    private void ensureTenantPool(String dbName) {
        if (routingDataSource.hasTenant(dbName)) return;
        synchronized (routingDataSource) {
            if (routingDataSource.hasTenant(dbName)) return;
            HikariDataSource ds = TenantDataSourceFactory.createTenant(
                    tenantDbSettings.buildJdbcUrl(dbName),
                    tenantDbSettings.username(),
                    tenantDbSettings.password(),
                    dbName);
            routingDataSource.addTenant(dbName, ds);
        }
    }

    /**
     * Phase A 스모크 테스트 전용 — Aggregator 우회하고 메타 DB 활성 회사 목록만 반환.
     * 컨트롤러가 가드 통과 검증 + mapper 등록 확인용으로 호출.
     */
    public List<Map<String, Object>> listActiveCompaniesForSmokeTest() {
        List<Map<String, Object>> rows = sqlSession.selectList("provisioning.listActiveCompanies");
        // 응답 키 순서 안정화 (Map 직렬화 시 순서 보장 위해 LinkedHashMap 으로 복사)
        List<Map<String, Object>> out = new ArrayList<>(rows.size());
        for (Map<String, Object> r : rows) {
            Map<String, Object> ordered = new LinkedHashMap<>();
            ordered.put("company_code", r.get("company_code"));
            ordered.put("company_name", r.get("company_name"));
            ordered.put("db_name", r.get("db_name"));
            out.add(ordered);
        }
        return out;
    }
}
