import cron from "node-cron";
import { PropertyService } from "./propertyService";
import { EmailService } from "./emailService";
import pool from "../config/db";
import { ResultSetHeader, RowDataPacket } from "mysql2";
import { scrapeRunCoordinator } from "./scrapeRunCoordinator";
import { getApplicationTimezone } from "../config/time";
import { ScrapingPortalRunModel } from "../models/scrapingPortalRunModel";

let lastRunAt: Date | null = null;
let lastRunStatus: "success" | "error" | null = null;
let lastRunTotal = 0;
let lastRunNew = 0;
let nextRunAt: Date | null = null;

export function parseDailyTime(value = "01:00"): { hour: number; minute: number } {
  const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
  if (!match) throw new Error(`SCRAPING_DAILY_TIME inválido: "${value}" (formato esperado HH:mm)`);
  const hour = Number(match[1]);
  const minute = Number(match[2]);
  if (hour > 23 || minute > 59) throw new Error(`SCRAPING_DAILY_TIME fuera de rango: "${value}"`);
  return { hour, minute };
}

/** Calcula la siguiente hora diaria respetando la zona y cambios DST. */
export function getNextDailyRunAt(
  now: Date,
  hour: number,
  minute: number,
  timezone: string
): Date {
  const formatter = new Intl.DateTimeFormat("en-GB", {
    timeZone: timezone,
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  });
  const candidate = new Date(now);
  candidate.setUTCSeconds(0, 0);
  candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
  for (let i = 0; i < 60 * 49; i++) {
    const parts = Object.fromEntries(formatter.formatToParts(candidate).map(p => [p.type, p.value]));
    if (Number(parts.hour) === hour && Number(parts.minute) === minute) return candidate;
    candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
  }
  throw new Error(`No se pudo calcular la próxima ejecución para ${timezone}`);
}

function getScheduledOperations(): Array<"sale" | "rent"> {
  const configured = (process.env.SCRAPING_OPERATIONS || "rent,sale")
    .split(",")
    .map(value => value.trim().toLowerCase())
    .filter((value): value is "sale" | "rent" => value === "sale" || value === "rent");
  return configured.length > 0 ? [...new Set(configured)] : ["rent", "sale"];
}

async function logScrapingStart(): Promise<number> {
  try {
    const [result] = await pool.query<ResultSetHeader>(
      "INSERT INTO scraping_log (started_at, status) VALUES (NOW(), 'running')"
    );
    return result.insertId;
  } catch (_e) { return 0; }
}

async function logScrapingEnd(
  logId: number,
  total: number,
  newSaved: number,
  errors: string,
  status: "success" | "error"
): Promise<void> {
  try {
    await pool.query(
      `UPDATE scraping_log SET finished_at = NOW(), total_found = ?, new_saved = ?, errors = ?, details = ?, status = ? WHERE id = ?`,
      [total, newSaved, status === "error" ? 1 : 0, errors || null, status, logId]
    );
  } catch (_e) { /* non-critical */ }
}

async function closeInterruptedLogs(): Promise<void> {
  try {
    const [result] = await pool.query<ResultSetHeader>(
      `UPDATE scraping_log SET finished_at = NOW(), status = 'error', errors = 1,
         details = COALESCE(details, 'Ejecución interrumpida por reinicio del proceso')
       WHERE status = 'running' AND finished_at IS NULL`
    );
    if (result.affectedRows > 0) {
      console.warn(`[Scheduler] ${result.affectedRows} ejecución(es) huérfana(s) cerrada(s)`);
    }
  } catch (_e) { /* non-critical */ }
}

export const startScheduler = (): void => {
  const timezone = process.env.SCRAPING_TIMEZONE || getApplicationTimezone();
  const { hour, minute } = parseDailyTime(process.env.SCRAPING_DAILY_TIME || "01:00");
  const cronExpression = `${minute} ${hour} * * *`;

  console.log(`Scheduler iniciado: scraping diario a las ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} (${timezone})`);
  void closeInterruptedLogs();
  void ScrapingPortalRunModel.closeInterrupted(
    "Ejecución interrumpida por reinicio del proceso"
  ).then((n) => {
    if (n > 0) console.warn(`[Scheduler] ${n} portal run(s) huérfano(s) cerrado(s)`);
  }).catch(() => { /* non-critical */ });

  // Calcular próxima ejecución
  const calcNextRun = () => {
    nextRunAt = getNextDailyRunAt(new Date(), hour, minute, timezone);
  };
  calcNextRun();

  cron.schedule(cronExpression, async () => {
    if (!scrapeRunCoordinator.tryStart("scheduled")) {
      console.log("Scraping ya en ejecución, saltando...");
      return;
    }
    const logId = await logScrapingStart();
    let errorMsg = "";
    try {
      const propertyService = new PropertyService();
      const emailService = new EmailService();
      const operations = getScheduledOperations();
      lastRunTotal = 0;
      lastRunNew = 0;
      console.log(`Iniciando scraping programado: ${operations.join(", ")}...`);
      for (const operation of operations) {
        console.log(`Iniciando ciclo programado ${operation}...`);
        const result = await propertyService.scrapeAll(operation);
        lastRunTotal += result.total;
        lastRunNew += result.new_count || 0;
        console.log(`Ciclo ${operation} completado: ${result.total} propiedades, ${result.new_count || 0} nuevas/actualizadas`);
      }
      console.log(`Scraping programado completado: ${lastRunTotal} propiedades, ${lastRunNew} nuevas/actualizadas`);
      await emailService.sendFilteredAlerts();
      lastRunStatus = "success";
      await logScrapingEnd(logId, lastRunTotal, lastRunNew, "", "success");
    } catch (err: any) {
      errorMsg = err?.message || String(err);
      console.error("Error en scraping programado:", errorMsg);
      lastRunStatus = "error";
      await logScrapingEnd(logId, lastRunTotal, lastRunNew, errorMsg, "error");
    } finally {
      scrapeRunCoordinator.finish("scheduled");
      lastRunAt = new Date();
      calcNextRun();
    }
  }, { timezone });

  // Purga diaria a las 3:00 — elimina del archivo los anuncios con más de 30 días
  cron.schedule("0 3 * * *", async () => {
    try {
      const PM = await import("../models/propertyModel");
      const PropertyModel = PM.PropertyModel;
      const n = await PropertyModel.purgeOldDeleted();
      if (n > 0) {
        console.log(`[Purge] ${n} propiedades eliminadas definitivamente del archivo (>30 días)`);
      }
    } catch (e: any) {
      console.error("[Purge] Error en purga diaria:", e?.message || String(e));
    }
  }, { timezone });
};

export const getSchedulerStatus = (): {
  running: boolean;
  lastRunAt: string | null;
  lastRunStatus: string | null;
  lastRunTotal: number;
  lastRunNew: number;
  nextRunAt: string | null;
} => ({
  running: scrapeRunCoordinator.status().mode === "scheduled",
  lastRunAt: lastRunAt ? lastRunAt.toISOString() : null,
  lastRunStatus,
  lastRunTotal,
  lastRunNew,
  nextRunAt: nextRunAt ? nextRunAt.toISOString() : null,
});

export async function getRecentScrapingLogs(limit = 10): Promise<RowDataPacket[]> {
  try {
    const [rows] = await pool.query<RowDataPacket[]>(
      "SELECT * FROM scraping_log ORDER BY started_at DESC LIMIT ?",
      [limit]
    );
    return rows;
  } catch (_e) { return []; }
}

