import { query, queryOne } from "../database/db";
import { logger } from "../utils/logger";
import {
  BatchLookupRequest,
  BatchLookupResponse,
} from "../types/tableManagement";

interface CacheEntry {
  data: Map<string, any>;
  expiry: number;
  size: number;
  stats: { hits: number; misses: number; created: Date };
}

/**
 * 향상된 참조 테이블 데이터 캐싱 서비스
 * 작은 참조 테이블의 성능 최적화를 위한 메모리 캐시
 * - TTL 기반 만료 관리
 * - 테이블 크기 기반 자동 전략 선택
 * - 메모리 사용량 최적화
 * - 배경 갱신 지원
 */
export class ReferenceCacheService {
  private cache = new Map<string, CacheEntry>();
  private loadingPromises = new Map<string, Promise<Map<string, any>>>();

  // 설정값들
  private readonly SMALL_TABLE_THRESHOLD = 1000; // 1000건 이하는 전체 캐싱
  private readonly MEDIUM_TABLE_THRESHOLD = 5000; // 5000건 이하는 선택적 캐싱
  private readonly TTL = 10 * 60 * 1000; // 10분 TTL
  private readonly BACKGROUND_REFRESH_THRESHOLD = 0.8; // TTL의 80% 지점에서 배경 갱신
  private readonly MAX_MEMORY_MB = 50; // 최대 50MB 메모리 사용

  /**
   * 테이블 크기 조회
   */
  private async getTableRowCount(tableName: string): Promise<number> {
    try {
      const countResult = await query<{ count: string }>(
        `SELECT COUNT(*) as count FROM ${tableName}`,
        []
      );

      return parseInt(countResult[0]?.count || "0", 10);
    } catch (error) {
      logger.error(`테이블 크기 조회 실패: ${tableName}`, error);
      return 0;
    }
  }

  /**
   * 캐시 전략 결정
   */
  private determineCacheStrategy(
    rowCount: number
  ): "full_cache" | "selective_cache" | "no_cache" {
    if (rowCount <= this.SMALL_TABLE_THRESHOLD) {
      return "full_cache";
    } else if (rowCount <= this.MEDIUM_TABLE_THRESHOLD) {
      return "selective_cache";
    } else {
      return "no_cache";
    }
  }

  /**
   * 참조 테이블 캐시 조회 (자동 로딩 포함)
   */
  async getCachedReference(
    tableName: string,
    keyColumn: string,
    displayColumn: string
  ): Promise<Map<string, any> | null> {
    const cacheKey = `${tableName}.${keyColumn}.${displayColumn}`;
    const cached = this.cache.get(cacheKey);
    const now = Date.now();

    // 캐시가 있고 만료되지 않았으면 반환
    if (cached && cached.expiry > now) {
      cached.stats.hits++;

      // 배경 갱신 체크 (TTL의 80% 지점)
      const age = now - cached.stats.created.getTime();
      if (age > this.TTL * this.BACKGROUND_REFRESH_THRESHOLD) {
        // 배경에서 갱신 시작 (비동기)
        this.refreshCacheInBackground(
          tableName,
          keyColumn,
          displayColumn
        ).catch((err) => logger.warn(`배경 캐시 갱신 실패: ${cacheKey}`, err));
      }

      return cached.data;
    }

    // 이미 로딩 중인 경우 기존 Promise 반환
    if (this.loadingPromises.has(cacheKey)) {
      return await this.loadingPromises.get(cacheKey)!;
    }

    // 테이블 크기 확인 후 전략 결정
    const rowCount = await this.getTableRowCount(tableName);
    const strategy = this.determineCacheStrategy(rowCount);

    if (strategy === "no_cache") {
      logger.debug(
        `테이블이 너무 큼, 캐싱하지 않음: ${tableName} (${rowCount}건)`
      );
      return null;
    }

    // 새로운 데이터 로드
    const loadPromise = this.loadReferenceData(
      tableName,
      keyColumn,
      displayColumn
    );
    this.loadingPromises.set(cacheKey, loadPromise);

    try {
      const result = await loadPromise;
      return result;
    } finally {
      this.loadingPromises.delete(cacheKey);
    }
  }

  /**
   * 실제 참조 데이터 로드
   */
  private async loadReferenceData(
    tableName: string,
    keyColumn: string,
    displayColumn: string
  ): Promise<Map<string, any>> {
    const cacheKey = `${tableName}.${keyColumn}.${displayColumn}`;

    try {
      logger.info(`참조 테이블 캐싱 시작: ${tableName}`);

      // 데이터 조회
      const data = await query<{ key: any; value: any }>(
        `SELECT ${keyColumn} as key, ${displayColumn} as value 
         FROM ${tableName}
         WHERE ${keyColumn} IS NOT NULL 
         AND ${displayColumn} IS NOT NULL
         ORDER BY ${keyColumn}`,
        []
      );

      const dataMap = new Map<string, any>();
      for (const row of data) {
        dataMap.set(String(row.key), row.value);
      }

      // 메모리 사용량 계산 (근사치)
      const estimatedSize = data.length * 50; // 대략 50바이트 per row

      // 캐시에 저장
      this.cache.set(cacheKey, {
        data: dataMap,
        expiry: Date.now() + this.TTL,
        size: estimatedSize,
        stats: { hits: 0, misses: 0, created: new Date() },
      });

      logger.info(
        `참조 테이블 캐싱 완료: ${tableName} (${data.length}건, ~${Math.round(estimatedSize / 1024)}KB)`
      );

      // 메모리 사용량 체크
      this.checkMemoryUsage();

      return dataMap;
    } catch (error) {
      logger.error(`참조 테이블 캐싱 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 배경에서 캐시 갱신
   */
  private async refreshCacheInBackground(
    tableName: string,
    keyColumn: string,
    displayColumn: string
  ): Promise<void> {
    try {
      logger.debug(`배경 캐시 갱신 시작: ${tableName}`);
      await this.loadReferenceData(tableName, keyColumn, displayColumn);
      logger.debug(`배경 캐시 갱신 완료: ${tableName}`);
    } catch (error) {
      logger.warn(`배경 캐시 갱신 실패: ${tableName}`, error);
    }
  }

  /**
   * 메모리 사용량 체크 및 정리
   */
  private checkMemoryUsage(): void {
    const totalSize = Array.from(this.cache.values()).reduce(
      (sum, entry) => sum + entry.size,
      0
    );
    const totalSizeMB = totalSize / (1024 * 1024);

    if (totalSizeMB > this.MAX_MEMORY_MB) {
      logger.warn(
        `캐시 메모리 사용량 초과: ${totalSizeMB.toFixed(2)}MB / ${this.MAX_MEMORY_MB}MB`
      );
      this.evictLeastUsedCaches();
    }
  }

  /**
   * 가장 적게 사용된 캐시 제거
   */
  private evictLeastUsedCaches(): void {
    const entries = Array.from(this.cache.entries())
      .map(([key, entry]) => ({
        key,
        entry,
        score:
          entry.stats.hits / Math.max(entry.stats.hits + entry.stats.misses, 1), // 히트율
      }))
      .sort((a, b) => a.score - b.score); // 낮은 히트율부터

    const toEvict = Math.ceil(entries.length * 0.3); // 30% 제거
    for (let i = 0; i < toEvict && i < entries.length; i++) {
      this.cache.delete(entries[i].key);
      logger.debug(
        `캐시 제거됨: ${entries[i].key} (히트율: ${(entries[i].score * 100).toFixed(1)}%)`
      );
    }
  }

  /**
   * 캐시에서 참조 값 조회 (동기식)
   */
  getLookupValue(
    table: string,
    keyColumn: string,
    displayColumn: string,
    key: string
  ): any | null {
    const cacheKey = `${table}.${keyColumn}.${displayColumn}`;
    const cached = this.cache.get(cacheKey);

    if (!cached || cached.expiry < Date.now()) {
      // 캐시 미스 또는 만료
      if (cached) {
        cached.stats.misses++;
      }
      return null;
    }

    const value = cached.data.get(String(key));
    if (value !== undefined) {
      cached.stats.hits++;
      return value;
    } else {
      cached.stats.misses++;
      return null;
    }
  }

  /**
   * 배치 룩업 (성능 최적화)
   */
  async batchLookup(
    requests: BatchLookupRequest[]
  ): Promise<BatchLookupResponse[]> {
    const responses: BatchLookupResponse[] = [];
    const missingLookups = new Map<string, BatchLookupRequest[]>();

    // 캐시에서 먼저 조회
    for (const request of requests) {
      const cacheKey = `${request.table}.${request.key}.${request.displayColumn}`;
      const value = this.getLookupValue(
        request.table,
        request.key,
        request.displayColumn,
        request.key
      );

      if (value !== null) {
        responses.push({ key: request.key, value });
      } else {
        // 캐시 미스 - DB 조회 필요
        if (!missingLookups.has(request.table)) {
          missingLookups.set(request.table, []);
        }
        missingLookups.get(request.table)!.push(request);
      }
    }

    // 캐시 미스된 항목들 DB에서 조회
    for (const [tableName, missingRequests] of missingLookups) {
      try {
        const keys = missingRequests.map((req) => req.key);
        const displayColumn = missingRequests[0].displayColumn; // 같은 테이블이므로 동일

        const data = await query<{ key: any; value: any }>(
          `SELECT key_column as key, ${displayColumn} as value 
           FROM ${tableName}
           WHERE key_column = ANY($1)`,
          [keys]
        );

        // 결과를 응답에 추가
        for (const row of data) {
          responses.push({ key: String(row.key), value: row.value });
        }

        // 없는 키들은 null로 응답
        const foundKeys = new Set(data.map((row) => String(row.key)));
        for (const req of missingRequests) {
          if (!foundKeys.has(req.key)) {
            responses.push({ key: req.key, value: null });
          }
        }
      } catch (error) {
        logger.error(`배치 룩업 실패: ${tableName}`, error);

        // 에러 발생 시 null로 응답
        for (const req of missingRequests) {
          responses.push({ key: req.key, value: null });
        }
      }
    }

    return responses;
  }

  /**
   * 캐시 적중률 조회
   */
  getCacheHitRate(
    table: string,
    keyColumn: string,
    displayColumn: string
  ): number {
    const cacheKey = `${table}.${keyColumn}.${displayColumn}`;
    const cached = this.cache.get(cacheKey);

    if (!cached || cached.stats.hits + cached.stats.misses === 0) {
      return 0;
    }

    return cached.stats.hits / (cached.stats.hits + cached.stats.misses);
  }

  /**
   * 전체 캐시 적중률 조회
   */
  getOverallCacheHitRate(): number {
    let totalHits = 0;
    let totalRequests = 0;

    for (const entry of this.cache.values()) {
      totalHits += entry.stats.hits;
      totalRequests += entry.stats.hits + entry.stats.misses;
    }

    return totalRequests > 0 ? totalHits / totalRequests : 0;
  }

  /**
   * 캐시 무효화
   */
  invalidateCache(
    table?: string,
    keyColumn?: string,
    displayColumn?: string
  ): void {
    if (table && keyColumn && displayColumn) {
      const cacheKey = `${table}.${keyColumn}.${displayColumn}`;
      this.cache.delete(cacheKey);
      logger.info(`캐시 무효화: ${cacheKey}`);
    } else {
      // 전체 캐시 무효화
      this.cache.clear();
      logger.info("전체 캐시 무효화");
    }
  }

  /**
   * 향상된 캐시 상태 조회
   */
  getCacheInfo(): Array<{
    cacheKey: string;
    dataSize: number;
    memorySizeKB: number;
    hitRate: number;
    expiresIn: number;
    created: Date;
    strategy: string;
  }> {
    const info: Array<{
      cacheKey: string;
      dataSize: number;
      memorySizeKB: number;
      hitRate: number;
      expiresIn: number;
      created: Date;
      strategy: string;
    }> = [];

    const now = Date.now();

    for (const [cacheKey, entry] of this.cache) {
      const hitRate =
        entry.stats.hits + entry.stats.misses > 0
          ? entry.stats.hits / (entry.stats.hits + entry.stats.misses)
          : 0;

      const expiresIn = Math.max(0, entry.expiry - now);

      info.push({
        cacheKey,
        dataSize: entry.data.size,
        memorySizeKB: Math.round(entry.size / 1024),
        hitRate,
        expiresIn,
        created: entry.stats.created,
        strategy:
          entry.data.size <= this.SMALL_TABLE_THRESHOLD
            ? "full_cache"
            : "selective_cache",
      });
    }

    return info.sort((a, b) => b.hitRate - a.hitRate);
  }

  /**
   * 캐시 성능 요약 정보
   */
  getCachePerformanceSummary(): {
    totalCaches: number;
    totalMemoryKB: number;
    overallHitRate: number;
    expiredCaches: number;
    averageAge: number;
  } {
    const now = Date.now();
    let totalMemory = 0;
    let expiredCount = 0;
    let totalAge = 0;

    for (const entry of this.cache.values()) {
      totalMemory += entry.size;
      if (entry.expiry < now) {
        expiredCount++;
      }
      totalAge += now - entry.stats.created.getTime();
    }

    return {
      totalCaches: this.cache.size,
      totalMemoryKB: Math.round(totalMemory / 1024),
      overallHitRate: this.getOverallCacheHitRate(),
      expiredCaches: expiredCount,
      averageAge:
        this.cache.size > 0 ? Math.round(totalAge / this.cache.size / 1000) : 0, // 초 단위
    };
  }

  /**
   * 자주 사용되는 참조 테이블들 자동 캐싱
   */
  async autoPreloadCommonTables(): Promise<void> {
    try {
      logger.info("공통 참조 테이블 자동 캐싱 시작");

      // 일반적인 참조 테이블들
      const commonTables = [
        { table: "user_info", key: "user_id", display: "user_name" },
        { table: "dept_info", key: "dept_code", display: "dept_name" },
        { table: "companies", key: "company_code", display: "company_name" },
      ];

      for (const { table, key, display } of commonTables) {
        try {
          await this.getCachedReference(table, key, display);
        } catch (error) {
          logger.warn(`공통 테이블 캐싱 실패 (무시함): ${table}`, error);
        }
      }

      logger.info("공통 참조 테이블 자동 캐싱 완료");
    } catch (error) {
      logger.error("공통 참조 테이블 자동 캐싱 실패", error);
    }
  }
}

export const referenceCacheService = new ReferenceCacheService();
