import { PropertyModel } from "../models/propertyModel";
import { PriceHistoryModel } from "../models/priceHistoryModel";
import { Property } from "../types/property";
import { Filter } from "../types/filter";
import { busLog, scraperBus } from "../utils/scraperEventBus";
import {
  IdealistaPlaywrightScraper, FotocasaPlaywrightScraper,
  HabitacliaScraper, PisosScraper, TrovatScraper,
  KyeroScraper, ThinkSpainScraper,
  MilanunciosScraper, YaencontreScraper, CasasapoScraper, VibboScraper, WallapopScraper,
  GlobalizaScraper, NuroaScraper, EngelVolkersScraper, TecnocasaScraper,
  EraSpainScraper, RemaxScraper, InmobiliariaScraper, SpainHousesScraper,
  HolpropScraper,
} from "../scrapers/index";
import { GeocodingService } from "./geocodingService";
import { ScoringService } from "./scoringService";
import { DeduplicationService } from "./deduplicationService";
import { PortalModel } from "../models/portalModel";
import { ZoneModel } from "../models/zoneModel";
import pool from "../config/db";
import { RowDataPacket } from "mysql2";
import { supportsScraperOperation } from "../scrapers/capabilities";
import { ScrapingPortalRunModel, PortalRunStatus } from "../models/scrapingPortalRunModel";
import { scrapeRunCoordinator } from "./scrapeRunCoordinator";

// Mapa de nombre de clase → constructor de scraper (Playwright > Cheerio para portales JS)
const SCRAPER_MAP: Record<string, new () => any> = {
  IdealistaScraper: IdealistaPlaywrightScraper,           // alias legacy → Playwright
  IdealistaPlaywrightScraper,                             // clave directa (valor actual en BD)
  FotocasaScraper: FotocasaPlaywrightScraper,             // alias legacy → Playwright
  FotocasaPlaywrightScraper,                              // clave directa (valor actual en BD)
  HabitacliaScraper, PisosScraper, TrovatScraper,
  KyeroScraper, ThinkSpainScraper,
  MilanunciosScraper, YaencontreScraper, CasasapoScraper, VibboScraper, WallapopScraper,
  GlobalizaScraper, NuroaScraper, EngelVolkersScraper, TecnocasaScraper,
  EraSpainScraper, RemaxScraper, InmobiliariaScraper, SpainHousesScraper,
  HolpropScraper,
};

const geoService = new GeocodingService();
const scoringService = new ScoringService();
const deduplicationService = new DeduplicationService();

export class PropertyService {
  async getAllProperties(): Promise<Property[]> {
    return PropertyModel.findAll();
  }

  async getFilteredProperties(filter: Partial<Filter>): Promise<Property[]> {
    return PropertyModel.filterAdvanced(
      filter.min_price, filter.max_price, filter.property_type,
      filter.municipality, filter.min_rooms, filter.min_size_m2
    );
  }

  async getPropertyById(id: string): Promise<Property | null> {
    return PropertyModel.findById(parseInt(id));
  }

  async createProperty(property: Property): Promise<Property> {
    await PropertyModel.upsert(property);
    return property;
  }

  async updateProperty(id: string, property: Property): Promise<Property | null> {
    const existing = await PropertyModel.findById(parseInt(id));
    if (!existing) return null;
    await PropertyModel.upsert({ ...existing, ...property });
    return property;
  }

  async deleteProperty(id: string): Promise<boolean> {
    const existing = await PropertyModel.findById(parseInt(id));
    if (!existing) return false;
    await PropertyModel.deleteById(parseInt(id));
    return true;
  }

  async scrapeAll(operationType: "sale" | "rent" = "sale"): Promise<{ total: number; new_count: number }> {
    // Construir lista de scrapers activos según la BD de portales
    let scraperClasses: string[] = [];
    try {
      const portals = await PortalModel.findActive();
      scraperClasses = portals.map(p => p.scraper_class);
    } catch (_e) {
      scraperClasses = Object.keys(SCRAPER_MAP);
    }

    const knownScraperClasses = scraperClasses.filter(cls => SCRAPER_MAP[cls]);
    const compatibleClasses = knownScraperClasses
      .filter(cls => supportsScraperOperation(cls, operationType));
    const unsupportedClasses = knownScraperClasses
      .filter(cls => !supportsScraperOperation(cls, operationType));
    const scrapers = compatibleClasses
      .map(cls => (new SCRAPER_MAP[cls]()).setOperationType(operationType));

    busLog("info", `Usando ${scrapers.length} portales activos (${operationType}): ${compatibleClasses.join(", ")}`);
    if (unsupportedClasses.length > 0) {
      busLog("info", `Omitidos por no soportar ${operationType}: ${unsupportedClasses.join(", ")}`);
    }

    let total = 0;
    let new_count = 0;
    const savedIds: number[] = [];

    for (const scraper of scrapers) {
      const portalName: string = scraper.constructor?.name ?? "Scraper";
      const runId = await this._startPortalRun(scraper, operationType);
      let portalNew = 0;
      try {
        busLog("info", `Iniciando scraping...`, portalName);
        scraperBus.progress({ portal: portalName, phase: "start" });
        const scrapeStartedAt = new Date();
        const props: Property[] = await scraper.scrape();
        total += props.length;
        busLog("info", `Obtenidas ${props.length} propiedades`, portalName);

        for (const prop of props) {
          const saved = await this._saveProperty(prop);
          if (saved) {
            if (saved.isNew) { new_count++; portalNew++; }
            savedIds.push(saved.id);
          }
        }

        // Archivar anuncios del portal que ya no están publicados
        if (props.length > 0 && !scraper.isPartialCoverage()) {
          const src: string = scraper.source ?? props[0].source;
          const archived = await PropertyModel.archiveStaleBySource(src, operationType, scrapeStartedAt);
          if (archived > 0) busLog("info", `${archived} anuncio(s) archivados (ya no publicados)`, portalName);
        }

        await this._finishPortalRun(runId, props.length > 0 ? "success" : "empty", props.length, portalNew, scraper);
        busLog("success", `Completado: ${props.length} obtenidas, ${portalNew} nuevas`, portalName);
        scraperBus.progress({ portal: portalName, phase: "done", total: props.length, saved: portalNew });
      } catch (err) {
        await this._finishPortalRun(runId, "error", 0, portalNew, scraper, (err as Error).message);
        busLog("error", `Error: ${(err as Error).message}`, portalName);
        scraperBus.progress({ portal: portalName, phase: "error", msg: (err as Error).message });
      }
    }

    busLog("success", `Scraping completado: ${total} propiedades procesadas, ${new_count} guardadas/actualizadas`);

    // Post-procesado asíncrono: scoring + deduplicación
    this._postProcess();

    return { total, new_count };
  }

  /** Ejecuta el scraping de UN único portal por su scraper_class */
  async scrapePortal(scraperClass: string, operationType: "sale" | "rent" = "sale"): Promise<{ total: number; new_count: number }> {
    const ScraperCtor = SCRAPER_MAP[scraperClass];
    if (!ScraperCtor) throw new Error(`Scraper no encontrado: ${scraperClass}`);
    if (!supportsScraperOperation(scraperClass, operationType)) {
      throw new Error(`El scraper ${scraperClass} no soporta la operación '${operationType}'`);
    }
    const scraper = new ScraperCtor().setOperationType(operationType);
    const portalName = scraperClass.replace(/Scraper$/, "").replace(/Playwright$/, " (Playwright)");
    const runId = await this._startPortalRun(scraper, operationType);
    busLog("info", `Iniciando scraping...`, portalName);
    scraperBus.progress({ portal: portalName, phase: "start" });

    let total = 0;
    let new_count = 0;

    try {
      const scrapeStartedAt = new Date();
      const props: Property[] = await scraper.scrape();
      total = props.length;
      busLog("info", `Obtenidas ${props.length} propiedades del portal`, portalName);
      for (const prop of props) {
        const saved = await this._saveProperty(prop);
        if (saved?.isNew) new_count++;
      }
      // Archivar anuncios del portal que ya no están publicados
      if (props.length > 0 && !scraper.isPartialCoverage()) {
        const src: string = scraper.source ?? props[0].source;
        const archived = await PropertyModel.archiveStaleBySource(src, operationType, scrapeStartedAt);
        if (archived > 0) busLog("info", `${archived} anuncio(s) archivados (ya no publicados)`, portalName);
      }
      await this._finishPortalRun(runId, props.length > 0 ? "success" : "empty", total, new_count, scraper);
    } catch (err) {
      await this._finishPortalRun(runId, "error", total, new_count, scraper, (err as Error).message);
      busLog("error", `Error: ${(err as Error).message}`, portalName);
      scraperBus.progress({ portal: portalName, phase: "error", msg: (err as Error).message });
    }

    busLog("success", `Completado: ${total} obtenidas, ${new_count} guardadas`, portalName);
    scraperBus.progress({ portal: portalName, phase: "done", total, saved: new_count });
    this._postProcess(portalName);
    return { total, new_count };
  }

  /** Scraping restringido a una única zona geográfica (todos los portales activos) */
  async scrapeByZone(zoneSlug: string, operationType: "sale" | "rent" = "sale"): Promise<{ total: number; new_count: number }> {
    const zone = await ZoneModel.findBySlug(zoneSlug);
    if (!zone) throw new Error(`Zona '${zoneSlug}' no encontrada en la base de datos`);

    let scraperClasses: string[] = [];
    try {
      const portals = await PortalModel.findActive();
      scraperClasses = portals.map(p => p.scraper_class);
    } catch (_e) {
      scraperClasses = Object.keys(SCRAPER_MAP);
    }

    const knownScraperClasses = scraperClasses.filter(cls => SCRAPER_MAP[cls]);
    const compatibleClasses = knownScraperClasses
      .filter(cls => supportsScraperOperation(cls, operationType));
    const unsupportedClasses = knownScraperClasses
      .filter(cls => !supportsScraperOperation(cls, operationType));
    const scrapers = compatibleClasses
      .map(cls => (new SCRAPER_MAP[cls]()).setZoneOverride([zone]).setOperationType(operationType));

    busLog("info", `Scraping zona "${zone.name}" — ${scrapers.length} portales`);
    if (unsupportedClasses.length > 0) {
      busLog("info", `Omitidos por no soportar ${operationType}: ${unsupportedClasses.join(", ")}`);
    }

    let total = 0;
    let new_count = 0;

    for (const scraper of scrapers) {
      const portalName: string = scraper.constructor?.name ?? "Scraper";
      const runId = await this._startPortalRun(scraper, operationType, zone.slug);
      let portalNew = 0;
      try {
        busLog("info", `Iniciando scraping...`, portalName);
        scraperBus.progress({ portal: portalName, phase: "start" });
        const props: Property[] = await scraper.scrape();
        total += props.length;
        busLog("info", `Obtenidas ${props.length} propiedades`, portalName);
        for (const prop of props) {
          const saved = await this._saveProperty(prop);
          if (saved?.isNew) { new_count++; portalNew++; }
        }
        await this._finishPortalRun(runId, props.length > 0 ? "success" : "empty", props.length, portalNew, scraper);
        scraperBus.progress({ portal: portalName, phase: "done", total: props.length, saved: portalNew });
      } catch (err) {
        await this._finishPortalRun(runId, "error", 0, portalNew, scraper, (err as Error).message);
        busLog("error", `Error: ${(err as Error).message}`, portalName);
        scraperBus.progress({ portal: portalName, phase: "error", msg: (err as Error).message });
      }
    }

    busLog("success", `Zona "${zone.name}" completada: ${total} propiedades procesadas, ${new_count} guardadas`);
    this._postProcess(`zona:${zone.name}`);
    return { total, new_count };
  }

  /** Re-geocodifica propiedades que tienen coordenadas de centro de municipio exactas */
  async reGeocodeAll(): Promise<{ updated: number }> {
    // Usar centros de zona desde la BD en lugar de valores hardcodeados
    const zones = await ZoneModel.findActive();
    const centerCoords = zones.map(z => [z.lat, z.lng] as [number, number]);
    const isCenter = (lat: number, lng: number) =>
      centerCoords.some(([la, ln]) => Math.abs(la - lat) < 0.0001 && Math.abs(ln - lng) < 0.0001);

    const all = await PropertyModel.findAll();
    let updated = 0;
    for (const prop of all) {
      try {
        if (!prop.lat || !prop.lng || isCenter(prop.lat, prop.lng)) {
          const geoQuery = [prop.location, prop.municipality, "Valencia", "España"]
            .filter(Boolean).filter((v, i, a) => a.indexOf(v) === i).join(", ");
          let geo: { lat: number; lng: number } | null = null;
          try {
            geo = await geoService.getCoordinates(geoQuery);
          } catch {
            if (prop.municipality) {
              geo = await geoService.geocodeMunicipality(prop.municipality);
            }
          }
          if (geo) {
            await PropertyModel.upsert({ ...prop, lat: geo.lat, lng: geo.lng });
            updated++;
          }
          await new Promise(r => setTimeout(r, 1100));
        }
      } catch (_e) { /* skip */ }
    }
    return { updated };
  }

  // ─── Helpers privados ───────────────────────────────────────────────────────

  private async _startPortalRun(scraper: any, operation: "sale" | "rent", zoneScope?: string): Promise<number> {
    try {
      const coordinatorMode = scrapeRunCoordinator.status().mode;
      return await ScrapingPortalRunModel.start({
        portal: scraper.source || scraper.constructor?.name || "Scraper",
        scraperClass: scraper.constructor?.name || "Scraper",
        operation,
        triggerMode: coordinatorMode === "scheduled" ? "scheduled" : "manual",
        zoneScope: zoneScope || null,
      });
    } catch (error) {
      busLog("warn", `No se pudo registrar el inicio de ejecución: ${(error as Error).message}`);
      return 0;
    }
  }

  private async _finishPortalRun(
    id: number,
    status: Exclude<PortalRunStatus, "running">,
    totalFound: number,
    newSaved: number,
    scraper: any,
    error?: string
  ): Promise<void> {
    try {
      await ScrapingPortalRunModel.finish(id, {
        status,
        totalFound,
        newSaved,
        partialCoverage: Boolean(scraper.isPartialCoverage?.()),
        error: error || null,
      });
    } catch (runError) {
      busLog("warn", `No se pudo cerrar el registro de ejecución: ${(runError as Error).message}`);
    }
  }

  /** Geocodifica si falta, calcula precio/m², guarda en BD y registra historial.
   *  Devuelve el ID guardado o null si hubo error (duplicado u otro). */
  private async _saveProperty(prop: Property): Promise<{ id: number; isNew: boolean } | null> {
    try {
      if (!prop.lat && !prop.lng) {
        try {
          const geoQuery = ([prop.location, prop.municipality, "Valencia", "España"] as string[])
            .filter(Boolean).filter((v, i, a) => a.indexOf(v) === i).join(", ");
          const geo = await geoService.getCoordinates(geoQuery);
          prop.lat = geo.lat;
          prop.lng = geo.lng;
        } catch {
          if (prop.municipality) {
            const geo = await geoService.geocodeMunicipality(prop.municipality);
            if (geo) { prop.lat = geo.lat; prop.lng = geo.lng; }
          }
        }
      }
      if (prop.price > 0 && (prop.size_m2 || 0) > 0) {
        (prop as any).price_per_m2 = Math.round(prop.price / prop.size_m2!);
      }
      const saved = await PropertyModel.upsertWithStatus(prop);
      if (saved.id && prop.price > 0) {
        await PriceHistoryModel.recordIfChanged(saved.id, prop.price);
      }
      return saved.id ? saved : null;
    } catch {
      return null; // skip duplicates / constraint errors
    }
  }

  /** Lanza scoring + deduplicación de forma asíncrona sin bloquear la respuesta */
  private _postProcess(label = ""): void {
    setImmediate(async () => {
      try {
        busLog("info", "Calculando scores...", label);
        await scoringService.scoreAll();
        busLog("info", "Ejecutando deduplicación...", label);
        await deduplicationService.resetDuplicates();
        await deduplicationService.runDeduplication();
        busLog("success", "Post-procesado completado ✅", label);
      } catch (err) {
        busLog("error", `[Post-scraping] Error: ${(err as Error).message}`, label);
      }
    });
  }

  /** Datos de analytics para el dashboard */
  async getAnalytics(): Promise<any> {
    const [byPortal] = await pool.query<RowDataPacket[]>(`
      SELECT source, COUNT(*) AS count, AVG(price) AS avg_price, AVG(price_per_m2) AS avg_pm2
      FROM properties WHERE is_duplicate = 0 OR is_duplicate IS NULL
      GROUP BY source ORDER BY count DESC
    `);

    const [byMuni] = await pool.query<RowDataPacket[]>(`
      SELECT municipality, COUNT(*) AS count, AVG(price) AS avg_price,
             AVG(price_per_m2) AS avg_pm2, AVG(score) AS avg_score
      FROM properties WHERE is_duplicate = 0 OR is_duplicate IS NULL
      GROUP BY municipality ORDER BY count DESC
    `);

    const [byType] = await pool.query<RowDataPacket[]>(`
      SELECT property_type, COUNT(*) AS count, AVG(price) AS avg_price
      FROM properties WHERE is_duplicate = 0 OR is_duplicate IS NULL
      GROUP BY property_type ORDER BY count DESC
    `);

    const [summary] = await pool.query<RowDataPacket[]>(`
      SELECT COUNT(*) AS total,
             COUNT(CASE WHEN is_duplicate = 1 THEN 1 END) AS duplicates,
             AVG(price) AS avg_price, AVG(price_per_m2) AS avg_pm2,
             AVG(score) AS avg_score,
             MIN(price) AS min_price, MAX(price) AS max_price,
             (SELECT COUNT(*) FROM price_history) AS price_records
      FROM properties
    `);

    const priceDrops = await PriceHistoryModel.getPriceDropStats();
    const priceOverTime = await PriceHistoryModel.getOverallTrend(90);

    return {
      summary: summary[0],
      byPortal, byMuni, byType,
      priceDrops, priceOverTime,
    };
  }
}

export default new PropertyService();
