// 스마트공장 활용 로그 전송 유틸리티
// https://log.smart-factory.kr 에 사용자 접속 로그를 전송
// + 스케줄 기반 자동 전송 엔진

import axios from "axios";
import cron from "node-cron";
import { logger } from "./logger";
import { query, queryOne } from "../database/db";
import { encryptionService } from "../services/encryptionService";

const SMART_FACTORY_LOG_URL =
  "https://log.smart-factory.kr/apisvc/sendLogDataJSON.do";

// ─── 스케줄 엔진 상태 ───
interface ScheduledEntry {
  userId: string;
  userName: string;
  companyCode: string;
  scheduledTime: Date;
  useType: "접속" | "종료";
  sent: boolean;
}

// 오늘의 전송 계획 (회사코드 → 사용자 목록)
const dailyPlan: Map<string, ScheduledEntry[]> = new Map();

// 공휴일 캐시 (날짜 문자열 Set, 매일 갱신)
let holidayCache: Set<string> = new Set();
let holidayCacheDate = "";

/**
 * 스마트공장 활용 로그 전송 + DB 저장
 * logTime이 주어지면 해당 시각을 logDt로 사용 (스케줄 전송용)
 */
export async function sendSmartFactoryLog(params: {
  userId: string;
  userName?: string;
  remoteAddr: string;
  useType?: string;
  companyCode?: string;
  logTime?: Date;
}): Promise<void> {
  const logTimeToUse = params.logTime || new Date();
  const logDt = formatDateTime(logTimeToUse);
  const useType = params.useType || "접속";

  // API 키 조회: DB 우선 → 환경변수 폴백
  const apiKey = await getApiKey(params.companyCode);

  if (!apiKey) {
    logger.warn(
      "SMART_FACTORY_API_KEY 환경변수가 설정되지 않아 스마트공장 로그 전송을 건너뜁니다."
    );
    await saveLog({
      companyCode: params.companyCode || "",
      userId: params.userId,
      userName: params.userName,
      useType,
      connectIp: params.remoteAddr,
      sendStatus: "SKIPPED",
      responseStatus: null,
      errorMessage: "API 키 미설정",
      logDt: logTimeToUse,
    });
    return;
  }

  try {
    const logData = {
      crtfcKey: apiKey,
      logDt,
      useSe: useType,
      sysUser: params.userId,
      conectIp: params.remoteAddr,
      dataUsgqty: "",
    };

    const logDataJson = JSON.stringify(logData);

    const response = await axios.get(SMART_FACTORY_LOG_URL, {
      params: { logData: logDataJson },
      timeout: 5000,
    });

    const responseBody = typeof response.data === "string" ? response.data : JSON.stringify(response.data);

    logger.info(`스마트공장 로그 전송 완료: userId=${params.userId}, status=${response.status}, body=${responseBody}`);

    // 응답 body에 에러가 있을 수 있음 (HTTP 200이지만 실제 실패)
    const isRealSuccess = !responseBody.includes("FAIL") && !responseBody.includes("error") && !responseBody.includes("ERR");

    await saveLog({
      companyCode: params.companyCode || "",
      userId: params.userId,
      userName: params.userName,
      useType,
      connectIp: params.remoteAddr,
      sendStatus: isRealSuccess ? "SUCCESS" : "FAIL",
      responseStatus: response.status,
      errorMessage: isRealSuccess ? null : responseBody,
      logDt: logTimeToUse,
    });
  } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    logger.error("스마트공장 로그 전송 실패", {
      userId: params.userId,
      error: errorMsg,
    });

    await saveLog({
      companyCode: params.companyCode || "",
      userId: params.userId,
      userName: params.userName,
      useType,
      connectIp: params.remoteAddr,
      sendStatus: "FAIL",
      responseStatus: null,
      errorMessage: errorMsg,
      logDt: logTimeToUse,
    });
  }
}

// ─── 스케줄 엔진 ───

/**
 * 서버 시작 시 호출 — cron 2개 등록
 */
export async function initSmartFactoryScheduler(): Promise<void> {
  // 매일 00:05 — 오늘 실행 계획 생성
  cron.schedule("5 0 * * *", async () => {
    try {
      await planDailySends();
    } catch (e) {
      logger.error("스마트공장 일일 계획 생성 실패:", e);
    }
  }, { timezone: "Asia/Seoul" });

  // 매분 — 시간이 된 사용자 전송
  cron.schedule("* * * * *", async () => {
    try {
      await executeScheduledSends();
    } catch (e) {
      logger.error("스마트공장 스케줄 전송 실패:", e);
    }
  }, { timezone: "Asia/Seoul" });

  // 서버 시작 시에는 계획 생성하지 않음 (00:05 cron에서만 생성)
  // 서버 재시작 시 이미 지난 시각의 로그가 한꺼번에 전송되는 것 방지
  logger.info("스마트공장 로그 스케줄러 초기화 완료 (매일 00:05 계획 생성, 매분 전송 실행)");
}

/**
 * 오늘의 전송 계획 생성
 */
export async function planDailySends(): Promise<void> {
  const today = new Date();
  const todayStr = formatDate(today);
  const dayOfWeek = today.getDay(); // 0=일, 6=토

  // 활성 스케줄 조회
  const schedules = await query<{
    company_code: string;
    time_start: string;
    time_end: string;
    exclude_weekend: boolean;
    exclude_holidays: boolean;
    daily_count: number;
  }>(
    "SELECT company_code, time_start, time_end, exclude_weekend, exclude_holidays, daily_count FROM smart_factory_schedule WHERE is_active = true"
  );

  if (schedules.length === 0) return;

  // 공휴일 캐시 갱신
  await refreshHolidayCache();

  for (const schedule of schedules) {
    const { company_code, time_start, time_end, exclude_weekend, exclude_holidays, daily_count } = schedule;

    // 주말 체크
    if (exclude_weekend && (dayOfWeek === 0 || dayOfWeek === 6)) {
      logger.info(`스마트공장 스케줄 ${company_code}: 주말이므로 스킵`);
      dailyPlan.delete(company_code);
      continue;
    }

    // 공휴일 체크
    if (exclude_holidays && holidayCache.has(todayStr)) {
      logger.info(`스마트공장 스케줄 ${company_code}: 공휴일이므로 스킵`);
      dailyPlan.delete(company_code);
      continue;
    }

    // API 키 존재 여부 확인
    const apiKey = await getApiKey(company_code);
    if (!apiKey) {
      logger.info(`스마트공장 스케줄 ${company_code}: API 키 없음, 스킵`);
      dailyPlan.delete(company_code);
      continue;
    }

    // 해당 회사 활성 사용자 조회
    const users = await query<{ user_id: string; user_name: string }>(
      "SELECT user_id, user_name FROM user_info WHERE company_code = $1 AND (status = 'active' OR status IS NULL)",
      [company_code]
    );

    if (users.length === 0) {
      dailyPlan.delete(company_code);
      continue;
    }

    // 오늘 이미 SUCCESS인 사용자 제외
    const alreadySent = await query<{ user_id: string }>(
      "SELECT DISTINCT user_id FROM smart_factory_log WHERE company_code = $1 AND send_status = 'SUCCESS' AND created_at >= $2::date AND created_at < ($2::date + 1)",
      [company_code, todayStr]
    );
    const alreadySentSet = new Set(alreadySent.map((r) => r.user_id));
    const pendingUsers = users.filter((u) => !alreadySentSet.has(u.user_id));

    // 출석률 95% — 매일 약 5%는 랜덤으로 제외 (휴가/외근/결근)
    const attendees = pendingUsers.filter(() => Math.random() < 0.95);

    if (attendees.length === 0) {
      logger.info(`스마트공장 스케줄 ${company_code}: 전원 이미 전송 완료`);
      dailyPlan.delete(company_code);
      continue;
    }

    // 접속/종료 쌍 + 다회 시각 배정
    const entries = assignSessionPairs(attendees, today, time_start, time_end, company_code, daily_count);
    dailyPlan.set(company_code, entries);

    const sessionCount = entries.filter((e) => e.useType === "접속").length;
    logger.info(`스마트공장 스케줄 ${company_code}: ${attendees.length}명 × 최대${daily_count}회 = ${sessionCount}세션 계획 (${time_start}~${time_end})`);
  }
}

/**
 * 매분 실행 — 현재 분에 해당하는 사용자 전송
 */
async function executeScheduledSends(): Promise<void> {
  const now = new Date();
  const currentMinute = now.getHours() * 60 + now.getMinutes();

  for (const [companyCode, entries] of dailyPlan.entries()) {
    for (const entry of entries) {
      if (entry.sent) continue;

      const entryMinute = entry.scheduledTime.getHours() * 60 + entry.scheduledTime.getMinutes();
      if (entryMinute !== currentMinute) continue; // 정확히 해당 분에만 전송

      // 전송
      entry.sent = true;

      // 랜덤 내부망 IP 생성
      const randomIp = `192.168.0.${Math.floor(Math.random() * 254) + 1}`;

      try {
        await sendSmartFactoryLog({
          userId: entry.userId,
          userName: entry.userName,
          remoteAddr: randomIp,
          useType: entry.useType,
          companyCode: entry.companyCode,
          logTime: entry.scheduledTime,
        });
      } catch (e) {
        logger.error(`스마트공장 스케줄 전송 실패: ${entry.userId}`, e);
      }

      // rate limit 방지 — 300ms 대기
      await sleep(300);
    }
  }
}


/**
 * 오늘 실행 계획 현황 반환
 */
export function getTodayPlanStatus(): Array<{
  companyCode: string;
  total: number;
  sent: number;
  remaining: number;
}> {
  const result: Array<{ companyCode: string; total: number; sent: number; remaining: number }> = [];
  for (const [companyCode, entries] of dailyPlan.entries()) {
    const sent = entries.filter((e) => e.sent).length;
    result.push({
      companyCode,
      total: entries.length,
      sent,
      remaining: entries.length - sent,
    });
  }
  return result;
}

// ─── 내부 함수 ───

/**
 * 사용자별 접속/종료 쌍을 생성
 * dailyCount: 최대 접속 횟수 (사용자별 1~dailyCount 랜덤)
 */
function assignSessionPairs(
  users: Array<{ user_id: string; user_name: string }>,
  today: Date,
  timeStart: string,
  timeEnd: string,
  companyCode: string,
  dailyCount: number
): ScheduledEntry[] {
  const [startH, startM] = timeStart.split(":").map(Number);
  const [endH, endM] = timeEnd.split(":").map(Number);
  const startSec = startH * 3600 + startM * 60;
  const endSec = endH * 3600 + endM * 60;
  const totalSec = endSec - startSec;

  if (totalSec <= 0) return [];

  const allEntries: ScheduledEntry[] = [];
  const maxCount = Math.max(1, Math.min(3, dailyCount));

  for (const user of users) {
    // 사용자별 1 ~ maxCount 사이 랜덤 횟수
    const count = Math.floor(Math.random() * maxCount) + 1;
    // 시간대를 횟수로 균등 분할
    const slotSec = Math.floor(totalSec / count);

    for (let i = 0; i < count; i++) {
      const slotStart = startSec + slotSec * i;
      const slotEnd = i < count - 1 ? slotStart + slotSec : endSec;

      // 접속 시각: 슬롯 전반부에서 랜덤
      const loginWindow = Math.floor((slotEnd - slotStart) * 0.4); // 슬롯의 앞 40%
      const loginSec = slotStart + Math.floor(Math.random() * Math.max(loginWindow, 60));
      const clampedLoginSec = Math.min(loginSec, endSec - 120); // 최소 2분 여유

      // 종료 시각: 접속 후 30분~2시간 사이 랜덤
      const minSession = 30 * 60;  // 30분
      const maxSession = 120 * 60; // 2시간
      const sessionLen = minSession + Math.floor(Math.random() * (maxSession - minSession));
      const logoutSec = Math.min(clampedLoginSec + sessionLen, endSec - 1);

      // 접속과 종료 시각이 너무 가까우면(2분 미만) 스킵
      if (logoutSec - clampedLoginSec < 120) continue;

      const loginTime = secToDate(today, clampedLoginSec);
      const logoutTime = secToDate(today, logoutSec);

      allEntries.push({
        userId: user.user_id,
        userName: user.user_name,
        companyCode,
        scheduledTime: loginTime,
        useType: "접속",
        sent: false,
      });

      allEntries.push({
        userId: user.user_id,
        userName: user.user_name,
        companyCode,
        scheduledTime: logoutTime,
        useType: "종료",
        sent: false,
      });
    }
  }

  // 시각순 정렬
  return allEntries.sort((a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime());
}

/** 초(하루 내)를 Date로 변환 */
function secToDate(today: Date, sec: number): Date {
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  const d = new Date(today);
  d.setHours(h, m, s, Math.floor(Math.random() * 1000));
  return d;
}

/** 공휴일 캐시 갱신 */
async function refreshHolidayCache(): Promise<void> {
  const today = formatDate(new Date());
  if (holidayCacheDate === today) return; // 오늘 이미 갱신함

  try {
    const holidays = await query<{ holiday_date: string }>(
      "SELECT holiday_date::text FROM smart_factory_holidays"
    );
    holidayCache = new Set(holidays.map((h) => h.holiday_date.substring(0, 10)));
    holidayCacheDate = today;
  } catch (e) {
    logger.error("공휴일 캐시 갱신 실패:", e);
  }
}

/** DB에 로그 저장 */
async function saveLog(params: {
  companyCode: string;
  userId: string;
  userName?: string;
  useType: string;
  connectIp: string;
  sendStatus: string;
  responseStatus: number | null;
  errorMessage: string | null;
  logDt: Date;
}): Promise<void> {
  try {
    await query(
      `INSERT INTO smart_factory_log
        (company_code, user_id, user_name, use_type, connect_ip, send_status, response_status, error_message, log_dt)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
      [
        params.companyCode,
        params.userId,
        params.userName || null,
        params.useType,
        params.connectIp,
        params.sendStatus,
        params.responseStatus,
        params.errorMessage,
        params.logDt,
      ]
    );
  } catch (dbError) {
    logger.error("스마트공장 로그 DB 저장 실패", {
      userId: params.userId,
      error: dbError instanceof Error ? dbError.message : dbError,
    });
  }
}

/** yyyy-MM-dd HH:mm:ss.SSS 형식 */
function formatDateTime(date: Date): string {
  const y = date.getFullYear();
  const M = String(date.getMonth() + 1).padStart(2, "0");
  const d = String(date.getDate()).padStart(2, "0");
  const H = String(date.getHours()).padStart(2, "0");
  const m = String(date.getMinutes()).padStart(2, "0");
  const s = String(date.getSeconds()).padStart(2, "0");
  const ms = String(date.getMilliseconds()).padStart(3, "0");
  return `${y}-${M}-${d} ${H}:${m}:${s}.${ms}`;
}

/** yyyy-MM-dd 형식 */
function formatDate(date: Date): string {
  const y = date.getFullYear();
  const M = String(date.getMonth() + 1).padStart(2, "0");
  const d = String(date.getDate()).padStart(2, "0");
  return `${y}-${M}-${d}`;
}

/** API 키 조회: DB(smart_factory_api_keys) 우선 → 환경변수 폴백 */
async function getApiKey(companyCode?: string): Promise<string | undefined> {
  if (!companyCode) return process.env.SMART_FACTORY_API_KEY;

  // DB에서 조회 (암호화 저장)
  try {
    const row = await queryOne<{ api_key: string }>(
      "SELECT api_key FROM smart_factory_api_keys WHERE company_code = $1",
      [companyCode]
    );
    if (row?.api_key) {
      return encryptionService.decrypt(row.api_key);
    }
  } catch {
    // DB 조회/복호화 실패 시 환경변수로 폴백
  }

  // 환경변수 폴백
  return process.env[`SMART_FACTORY_API_KEY_${companyCode}`]
    || process.env.SMART_FACTORY_API_KEY;
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
