import "server-only";

import { chromium } from "playwright";
import { read, utils } from "xlsx";
import { LicitationCategory, LicitationProcedureType, LicitationSource } from "@prisma/client";
import { licitationsConfig } from "@/lib/licitations/config";
import {
  mapCategory,
  mapProcedureType,
  parseAmountAndCurrency,
  parseDateMaybe,
  pickFirstText,
  pickPublishDate,
  sanitizeDocuments,
} from "@/lib/licitations/normalize";
import type { ConnectorResult, MunicipalityConnectorInput, NormalizedDocument, NormalizedLicitationInput } from "@/lib/licitations/types";
import { delay, normalizeUrl, withRetries } from "@/lib/licitations/utils";

const PNT_HOST = "https://consultapublicamx.plataformadetransparencia.org.mx";
const FRACCION_REGEX = /(fracci[oó]n\s*xxviii|procedimientos de adjudicaci[oó]n directa|invitaci[oó]n restringida|licitaci[oó]n)/i;

type ScrapedTableRow = {
  tableIndex: number;
  rowIndex: number;
  rowText: string;
  cells: string[];
  values: Record<string, string>;
  links: Array<{ name: string; url: string }>;
};

type ScrapedSnapshot = {
  pageUrl: string;
  pageTitle: string;
  rows: ScrapedTableRow[];
  downloadLinks: Array<{ name: string; url: string }>;
};

function maybeEncodeBase64(value: string | null) {
  if (!value) {
    return null;
  }

  if (/^[A-Za-z0-9+/=]+$/.test(value) && value.length % 4 === 0) {
    return value;
  }

  return Buffer.from(value, "utf-8").toString("base64");
}

function addCandidate(set: Set<string>, value: string | null) {
  if (!value) {
    return;
  }

  try {
    set.add(new URL(value, PNT_HOST).toString());
  } catch {
    // Ignore malformed URLs.
  }
}

export function resolvePntEntryCandidates(municipality: MunicipalityConnectorInput) {
  const candidates = new Set<string>();

  if (municipality.pntEntryUrl) {
    addCandidate(candidates, municipality.pntEntryUrl);
  }

  const encodedEntity = maybeEncodeBase64(municipality.pntEntityId);
  const encodedSubject = maybeEncodeBase64(municipality.pntSubjectId);

  if (encodedEntity && encodedSubject) {
    addCandidate(
      candidates,
      `${PNT_HOST}/vut-web/faces/view/consultaPublica.xhtml?idEntidad=${encodeURIComponent(encodedEntity)}&idSujetoObligado=${encodeURIComponent(encodedSubject)}#inicio`,
    );
  }

  if (municipality.pntEntityId && municipality.pntSubjectId) {
    const sector = municipality.pntSectorId ?? "";
    addCandidate(
      candidates,
      `${PNT_HOST}/vut-web/?idEntidadParametro=${encodeURIComponent(municipality.pntEntityId)}&idSectorParametro=${encodeURIComponent(sector)}&idSujetoObigadoParametro=${encodeURIComponent(municipality.pntSubjectId)}`,
    );
    addCandidate(
      candidates,
      `${PNT_HOST}/vut-web/?idEntidadParametro=${encodeURIComponent(municipality.pntEntityId)}&idSectorParametro=${encodeURIComponent(sector)}&idSujetoObligadoParametro=${encodeURIComponent(municipality.pntSubjectId)}`,
    );
  }

  addCandidate(candidates, `${PNT_HOST}/vut-web/`);

  return [...candidates];
}

function pickRowValue(row: ScrapedTableRow, patterns: RegExp[]) {
  for (const [key, value] of Object.entries(row.values)) {
    if (patterns.some((pattern) => pattern.test(key))) {
      const normalized = value.trim();

      if (normalized) {
        return normalized;
      }
    }
  }

  return null;
}

function normalizeLinks(links: Array<{ name: string; url: string }>, pageUrl: string): NormalizedDocument[] {
  return links
    .map((link) => {
      const url = normalizeUrl(link.url, pageUrl);

      if (!url) {
        return null;
      }

      const typeMatch = url.match(/\.([a-z0-9]+)(?:$|\?)/i);
      const document: NormalizedDocument = {
        name: link.name?.trim() || "Documento",
        url,
      };

      if (typeMatch?.[1]) {
        document.type = typeMatch[1].toUpperCase();
      }

      return document;
    })
    .filter((item): item is NormalizedDocument => item !== null);
}

function rowToLicitation(row: ScrapedTableRow, snapshot: ScrapedSnapshot): NormalizedLicitationInput | null {
  const title = pickFirstText([
    pickRowValue(row, [/objeto/i, /concepto/i, /descripcion/i, /convocatoria/i, /procedimiento/i]),
    row.cells[0],
    row.rowText,
  ]);

  if (!title) {
    return null;
  }

  const procedureText = pickFirstText([pickRowValue(row, [/tipo/i, /procedimiento/i]), row.rowText]);
  const categoryText = pickFirstText([pickRowValue(row, [/categoria/i, /rubro/i, /giro/i, /objeto/i]), row.rowText]);
  const publishDate = pickPublishDate([
    pickRowValue(row, [/fecha de publicacion/i, /publicaci[oó]n/i, /fecha de emision/i]),
    pickRowValue(row, [/fecha/i]),
  ]);

  const amountText = pickFirstText([pickRowValue(row, [/monto/i, /importe/i, /presupuesto/i]), row.rowText]);
  const amountData = parseAmountAndCurrency(amountText);

  const sourceRecordId = pickFirstText([
    pickRowValue(row, [/folio/i, /expediente/i, /id/i, /numero/i]),
    `${row.tableIndex}-${row.rowIndex}`,
  ]);

  const eventDates: Record<string, string> = {};

  for (const [key, value] of Object.entries(row.values)) {
    if (!/fecha/i.test(key)) {
      continue;
    }

    const parsed = parseDateMaybe(value);

    if (!parsed) {
      continue;
    }

    eventDates[key] = parsed.toISOString();
  }

  const documents = sanitizeDocuments(normalizeLinks(row.links, snapshot.pageUrl));

  return {
    sourceRecordId,
    procedureType: mapProcedureType(procedureText),
    title,
    description: pickRowValue(row, [/descripcion/i, /detalle/i, /observaciones/i]),
    category: mapCategory(categoryText),
    publishDate,
    eventDates: Object.keys(eventDates).length ? eventDates : null,
    amount: amountData.amount,
    currency: amountData.currency,
    status: pickRowValue(row, [/estatus/i, /estado/i, /situacion/i]),
    supplierAwarded: pickRowValue(row, [/proveedor/i, /adjudicado/i, /ganador/i]),
    documents,
    rawSourceUrl: snapshot.pageUrl,
    rawPayload: {
      pageTitle: snapshot.pageTitle,
      row,
    },
  };
}

function linkToStub(link: { name: string; url: string }, pageUrl: string, pageTitle: string): NormalizedLicitationInput | null {
  const normalizedUrl = normalizeUrl(link.url, pageUrl);

  if (!normalizedUrl) {
    return null;
  }

  return {
    sourceRecordId: normalizedUrl,
    procedureType: LicitationProcedureType.UNKNOWN,
    title: link.name?.trim() || "Documento de licitacion",
    description: "Registro generado desde enlace documental en PNT.",
    category: LicitationCategory.UNKNOWN,
    publishDate: null,
    documents: sanitizeDocuments([{ name: link.name || "Documento", url: normalizedUrl }]),
    rawSourceUrl: pageUrl,
    rawPayload: {
      pageTitle,
      link,
      mode: "link-feed",
    },
  };
}

async function parseSpreadsheetLinks(snapshot: ScrapedSnapshot) {
  const spreadsheetLinks = snapshot.downloadLinks.filter((link) => /\.(xlsx|xls|csv)(?:$|\?)/i.test(link.url));

  if (!spreadsheetLinks.length) {
    return [] as NormalizedLicitationInput[];
  }

  const records: NormalizedLicitationInput[] = [];

  for (const link of spreadsheetLinks) {
    try {
      const spreadsheetUrl = normalizeUrl(link.url, snapshot.pageUrl);

      if (!spreadsheetUrl) {
        continue;
      }

      const response = await fetch(spreadsheetUrl);

      if (!response.ok) {
        continue;
      }

      const arrayBuffer = await response.arrayBuffer();
      const workbook = read(arrayBuffer, { type: "array" });

      for (const sheetName of workbook.SheetNames) {
        const sheet = workbook.Sheets[sheetName];

        if (!sheet) {
          continue;
        }

        const rows = utils.sheet_to_json<Record<string, unknown>>(sheet, {
          defval: "",
        });

        for (const row of rows) {
          const text = Object.values(row)
            .map((value) => String(value ?? ""))
            .join(" ");

          if (!text.trim()) {
            continue;
          }

          const publishDate = pickPublishDate([
            String(row["Fecha de publicación"] ?? ""),
            String(row["Fecha"] ?? ""),
          ]);

          const amountData = parseAmountAndCurrency(String(row["Monto"] ?? row["Importe"] ?? ""));

          records.push({
            sourceRecordId: String(row["Folio"] ?? row["ID"] ?? row["No."] ?? "").trim() || null,
            procedureType: mapProcedureType(String(row["Procedimiento"] ?? row["Tipo"] ?? text)),
            title: pickFirstText([
              String(row["Objeto"] ?? ""),
              String(row["Descripción"] ?? row["Descripcion"] ?? ""),
              text,
            ]),
            description: String(row["Descripción"] ?? row["Descripcion"] ?? "").trim() || null,
            category: mapCategory(String(row["Categoría"] ?? row["Categoria"] ?? text)),
            publishDate,
            amount: amountData.amount,
            currency: amountData.currency,
            status: String(row["Estatus"] ?? row["Estado"] ?? "").trim() || null,
            supplierAwarded: String(row["Proveedor"] ?? row["Ganador"] ?? "").trim() || null,
            documents: [{ name: link.name || `Archivo ${sheetName}`, url: spreadsheetUrl, type: "XLSX" }],
            rawSourceUrl: spreadsheetUrl,
            rawPayload: {
              source: "spreadsheet",
              sheetName,
              row,
            },
          });
        }
      }
    } catch {
      // Non-fatal.
    }
  }

  return records;
}

async function openPntAndExtract(municipality: MunicipalityConnectorInput) {
  const browser = await chromium.launch({ headless: true });

  try {
    const context = await browser.newContext({
      userAgent:
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
    });
    const page = await context.newPage();

    const candidates = resolvePntEntryCandidates(municipality);
    let opened = false;

    for (const candidate of candidates) {
      try {
        await page.goto(candidate, { waitUntil: "domcontentloaded", timeout: 45_000 });
        opened = true;
        break;
      } catch {
        // Try next candidate.
      }
    }

    if (!opened) {
      throw new Error("PNT entry URL could not be opened.");
    }

    await page.waitForTimeout(1500);

    const target = page.getByText(FRACCION_REGEX).first();

    if ((await target.count()) > 0) {
      await target.click({ timeout: 10_000 }).catch(() => undefined);
      await page.waitForTimeout(1200);
    }

    const snapshot = (await page.evaluate(() => {
      const normalize = (value: string | null | undefined) => (value ?? "").replace(/\s+/g, " ").trim();

      const rows: Array<{
        tableIndex: number;
        rowIndex: number;
        rowText: string;
        cells: string[];
        values: Record<string, string>;
        links: Array<{ name: string; url: string }>;
      }> = [];

      const tableElements = Array.from(document.querySelectorAll("table"));

      tableElements.forEach((table, tableIndex) => {
        const headerCells = Array.from(table.querySelectorAll("thead th, tr th"));
        const headers = headerCells.map((cell, index) => normalize(cell.textContent) || `col_${index + 1}`);
        const bodyRows = Array.from(table.querySelectorAll("tbody tr, tr")).filter((row) => row.querySelectorAll("td").length > 0);

        bodyRows.forEach((row, rowIndex) => {
          const cells = Array.from(row.querySelectorAll("td")).map((cell) => normalize(cell.textContent));
          const values: Record<string, string> = {};

          cells.forEach((cellValue, index) => {
            const key = headers[index] || `col_${index + 1}`;
            values[key] = cellValue;
          });

          const links = Array.from(row.querySelectorAll("a[href]")).map((anchor) => ({
            name: normalize(anchor.textContent) || normalize(anchor.getAttribute("title")) || "Documento",
            url: String(anchor.getAttribute("href") || ""),
          }));

          rows.push({
            tableIndex,
            rowIndex,
            rowText: cells.join(" | "),
            cells,
            values,
            links,
          });
        });
      });

      const downloadLinks = Array.from(document.querySelectorAll("a[href]")).
        map((anchor) => ({
          name: normalize(anchor.textContent) || normalize(anchor.getAttribute("title")) || "Documento",
          url: String(anchor.getAttribute("href") || ""),
        }))
        .filter((item) => /\.(pdf|xls|xlsx|csv|doc|docx)(?:$|\?)/i.test(item.url));

      return {
        pageUrl: window.location.href,
        pageTitle: document.title || "PNT",
        rows,
        downloadLinks,
      };
    })) as ScrapedSnapshot;

    await context.close();
    return snapshot;
  } finally {
    await browser.close();
  }
}

export async function fetchPntLicitations(
  municipality: MunicipalityConnectorInput,
  options?: {
    targetYear?: number;
  },
): Promise<ConnectorResult> {
  const warnings: string[] = [];

  if (!municipality.pntSubjectId && !municipality.pntEntryUrl) {
    return {
      source: LicitationSource.PNT,
      items: [],
      warnings: ["Municipio sin identificador PNT configurado."],
    };
  }

  const snapshot = await withRetries(() => openPntAndExtract(municipality), {
    retries: licitationsConfig.maxRetries,
    initialDelayMs: 900,
  });

  await delay(licitationsConfig.requestDelayMs);

  const items = snapshot.rows
    .map((row) => rowToLicitation(row, snapshot))
    .filter((row): row is NormalizedLicitationInput => Boolean(row))
    .filter((row) => {
      if (!options?.targetYear || !row.publishDate) {
        return true;
      }

      return row.publishDate.getUTCFullYear() === options.targetYear;
    });

  const spreadsheetItems = await parseSpreadsheetLinks(snapshot);
  const merged = [...items, ...spreadsheetItems];

  if (!merged.length && snapshot.downloadLinks.length) {
    for (const link of snapshot.downloadLinks) {
      const stub = linkToStub(link, snapshot.pageUrl, snapshot.pageTitle);

      if (stub) {
        merged.push(stub);
      }
    }
  }

  if (!snapshot.rows.length) {
    warnings.push("No se detectaron tablas estructuradas en PNT para Fraccion XXVIII.");
  }

  return {
    source: LicitationSource.PNT,
    items: merged,
    rawSourceUrl: snapshot.pageUrl,
    warnings,
  };
}
