import "server-only";

import { request as httpsRequest } from "node:https";
import { LicitationSource } from "@prisma/client";
import { licitationsConfig } from "@/lib/licitations/config";
import { mapCategory, mapProcedureType, parseDateMaybe, pickFirstText, sanitizeDocuments } from "@/lib/licitations/normalize";
import type { ConnectorResult, NormalizedDocument, NormalizedLicitationInput } from "@/lib/licitations/types";
import { delay, normalizeUrl, withRetries } from "@/lib/licitations/utils";

const DEFAULT_BASE_URL = "https://www.licitaya.com.mx/api/v1";

function toText(value: unknown) {
  if (typeof value === "string") {
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : null;
  }

  if (typeof value === "number" || typeof value === "boolean") {
    return String(value);
  }

  return null;
}

function formatDateForQuery(date: Date) {
  const year = String(date.getUTCFullYear());
  const month = String(date.getUTCMonth() + 1).padStart(2, "0");
  const day = String(date.getUTCDate()).padStart(2, "0");
  return `${year}${month}${day}`;
}

function buildDocument(name: string, value: unknown, baseUrl: string): NormalizedDocument | null {
  const rawUrl = toText(value);

  if (!rawUrl) {
    return null;
  }

  const normalizedUrl = normalizeUrl(rawUrl, baseUrl);

  if (!normalizedUrl) {
    return null;
  }

  return {
    name,
    url: normalizedUrl,
  };
}

function mapLicitayaRecord(record: Record<string, unknown>, requestUrl: string): NormalizedLicitationInput | null {
  const tenderId = toText(record.tenderId);
  const title = pickFirstText([toText(record.tender_object), toText(record.expanded_search), tenderId ? `Licitacion ${tenderId}` : null]);

  if (!tenderId || !title) {
    return null;
  }

  const lotObjects = Array.isArray(record.lots)
    ? record.lots
        .map((lot) => (lot && typeof lot === "object" ? toText((lot as Record<string, unknown>).lot_object) : null))
        .filter((entry): entry is string => Boolean(entry))
        .slice(0, 8)
    : [];

  const description = pickFirstText([toText(record.expanded_search), toText(record.extra_info), lotObjects.join(" | "), null]) || null;
  const publishDate = parseDateMaybe(toText(record.catalog_date));
  const closingDate = parseDateMaybe(toText(record.close_date));
  const stateToken = toText(record.state);
  const documents = sanitizeDocuments(
    [
      buildDocument("Ficha LicitaYa", record.url, requestUrl),
      buildDocument("Fuente alterna", record.url2, requestUrl),
    ].filter((entry): entry is NormalizedDocument => entry !== null),
  );

  return {
    sourceRecordId: tenderId,
    tenderCode: pickFirstText([toText(record.number), toText(record.number2), toText(record.reference), toText(record.process), null]) || null,
    procedureType: mapProcedureType(toText(record.type)),
    title,
    description,
    category: mapCategory(`${title} ${description ?? ""}`),
    isOpen: closingDate ? closingDate >= new Date() : true,
    publishDate,
    closingDate,
    supplierAwarded: toText(record.agency),
    documents,
    rawSourceUrl: requestUrl,
    rawPayload: {
      source: "licitaya",
      licitayaState: stateToken,
      licitayaCity: toText(record.city),
      licitayaCountry: toText(record.country),
      record,
    },
  };
}

function resolveRuntimeConfig() {
  const apiKey = process.env.LICITAYA_API_KEY?.trim();

  if (!apiKey) {
    throw new Error("LICITAYA_API_KEY no esta configurada.");
  }

  const baseUrl = (process.env.LICITAYA_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
  return {
    apiKey,
    baseUrl,
  };
}

function buildHeaders(apiKey: string) {
  return {
    "X-API-KEY": apiKey,
    Accept: "application/json",
    "User-Agent": process.env.LICITAYA_USER_AGENT?.trim() || "undici",
    "Accept-Encoding": "identity",
    "Cache-Control": "no-cache",
    Pragma: "no-cache",
  };
}

type TextResponse = {
  status: number;
  body: string;
};

async function requestText(url: URL, headers: Record<string, string>, timeoutMs: number, redirectCount = 0): Promise<TextResponse> {
  return new Promise((resolve, reject) => {
    const req = httpsRequest(
      url,
      {
        method: "GET",
        headers,
      },
      (response) => {
        const status = response.statusCode ?? 0;
        const locationHeader = response.headers.location;

        if (locationHeader && [301, 302, 303, 307, 308].includes(status) && redirectCount < 3) {
          const redirectedUrl = new URL(locationHeader, url);
          requestText(redirectedUrl, headers, timeoutMs, redirectCount + 1).then(resolve).catch(reject);
          response.resume();
          return;
        }

        const chunks: Buffer[] = [];

        response.on("data", (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });

        response.on("end", () => {
          resolve({
            status,
            body: Buffer.concat(chunks).toString("utf8"),
          });
        });
      },
    );

    req.on("error", reject);
    req.setTimeout(timeoutMs, () => {
      req.destroy(new Error(`Request timed out after ${timeoutMs} ms.`));
    });
    req.end();
  });
}

function resolveTargetDate(offsetDays: number) {
  const now = new Date();
  now.setUTCDate(now.getUTCDate() - Math.max(0, offsetDays));
  return now;
}

function parseJsonPayload(rawBody: string) {
  const trimmed = rawBody.trim();

  if (!trimmed) {
    return null;
  }

  const sanitized = trimmed.startsWith("\uFEFF") ? trimmed.slice(1) : trimmed;

  try {
    return JSON.parse(sanitized) as Record<string, unknown>;
  } catch {
    return null;
  }
}

export async function fetchLicitayaLicitations(): Promise<ConnectorResult> {
  const runtime = resolveRuntimeConfig();
  const itemsPerPage = Math.max(1, Math.min(100, licitationsConfig.licitayaItemsPerPage));
  const maxPages = Math.max(1, licitationsConfig.licitayaMaxPagesPerRun);
  const startOffset = Math.max(0, licitationsConfig.licitayaDateOffsetDays);
  const fallbackWindow = Math.max(0, licitationsConfig.licitayaDateFallbackWindowDays);

  const deduped = new Map<string, NormalizedLicitationInput>();
  const warnings: string[] = [];
  const emptyDates: string[] = [];
  let rawSourceUrl: string | null = null;

  for (let fallbackIndex = 0; fallbackIndex <= fallbackWindow; fallbackIndex += 1) {
    const dateOffset = startOffset + fallbackIndex;
    const targetDate = formatDateForQuery(resolveTargetDate(dateOffset));

    let currentPage = 1;
    let totalPages = 1;
    let dateHadHardFailure = false;
    let dateHadNotFound = false;

    while (currentPage <= maxPages && currentPage <= totalPages) {
      const requestUrl = new URL("tender/search", `${runtime.baseUrl}/`);
      requestUrl.searchParams.set("date", targetDate);
      requestUrl.searchParams.set("items", String(itemsPerPage));
      requestUrl.searchParams.set("page", String(currentPage));
      requestUrl.searchParams.set("order", "1");
      rawSourceUrl = requestUrl.toString();

      let response: TextResponse;

      try {
        response = await withRetries(
          async () => requestText(requestUrl, buildHeaders(runtime.apiKey), licitationsConfig.licitayaTimeoutMs),
          {
            retries: licitationsConfig.maxRetries,
            initialDelayMs: Math.max(250, Math.floor(licitationsConfig.requestDelayMs / 2)),
            maxDelayMs: 5000,
          },
        );
      } catch (error) {
        const message = error instanceof Error ? error.message : "Error desconocido";
        warnings.push(`Error consultando LicitaYa (${targetDate}, pagina ${currentPage}): ${message}`);
        dateHadHardFailure = true;
        break;
      }

      if (response.status === 404) {
        dateHadNotFound = true;
        break;
      }

      if (response.status < 200 || response.status >= 300) {
        warnings.push(`LicitaYa devolvio ${response.status} para ${requestUrl.toString()}.`);
        dateHadHardFailure = true;
        break;
      }

      const rawBody = response.body;
      const payload = parseJsonPayload(rawBody);

      if (!payload) {
        const bodyPreview = rawBody.trim().slice(0, 200);
        warnings.push(`No se pudo parsear JSON de LicitaYa (${targetDate}, pagina ${currentPage}). Preview: ${bodyPreview || "(vacio)"}`);
        dateHadHardFailure = true;
        break;
      }

      const totalPagesCandidate = Number.parseInt(toText(payload.total_pages) ?? "", 10);

      if (Number.isFinite(totalPagesCandidate) && totalPagesCandidate > 0) {
        totalPages = totalPagesCandidate;
      }

      const pageResults = Array.isArray(payload.results) ? payload.results : [];

      for (const row of pageResults) {
        if (!row || typeof row !== "object") {
          continue;
        }

        const mapped = mapLicitayaRecord(row as Record<string, unknown>, requestUrl.toString());

        if (!mapped) {
          continue;
        }

        const key = mapped.sourceRecordId ?? mapped.title;
        deduped.set(key, mapped);
      }

      if (currentPage >= totalPages || currentPage >= maxPages) {
        break;
      }

      currentPage += 1;
      await delay(licitationsConfig.requestDelayMs);
    }

    if (deduped.size > 0) {
      if (fallbackIndex > 0) {
        warnings.push(`LicitaYa se sincronizo usando una fecha retroactiva (${targetDate}).`);
      }

      return {
        source: LicitationSource.LICITAYA,
        items: [...deduped.values()],
        rawSourceUrl,
        warnings,
      };
    }

    if (dateHadNotFound) {
      emptyDates.push(targetDate);
      continue;
    }

    if (dateHadHardFailure) {
      break;
    }
  }

  const fallbackTenderIds = (process.env.LICITAYA_FALLBACK_TENDER_IDS ?? "SCRZJ")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean)
    .slice(0, 5);

  for (const tenderId of fallbackTenderIds) {
    const requestUrl = new URL(`tender/${encodeURIComponent(tenderId)}`, `${runtime.baseUrl}/`);
    rawSourceUrl = requestUrl.toString();

    let response: TextResponse;

    try {
      response = await withRetries(
        async () => {
            return await requestText(requestUrl, buildHeaders(runtime.apiKey), licitationsConfig.licitayaTimeoutMs);
          },
        {
          retries: licitationsConfig.maxRetries,
          initialDelayMs: Math.max(250, Math.floor(licitationsConfig.requestDelayMs / 2)),
          maxDelayMs: 5000,
        },
      );
    } catch (error) {
      const message = error instanceof Error ? error.message : "Error desconocido";
      warnings.push(`Error consultando fallback de LicitaYa (${tenderId}): ${message}`);
      continue;
    }

    if (response.status < 200 || response.status >= 300) {
      warnings.push(`Fallback de LicitaYa devolvio ${response.status} para tender ${tenderId}.`);
      continue;
    }

    const rawBody = response.body;
    const payload = parseJsonPayload(rawBody);

    if (!payload) {
      const bodyPreview = rawBody.trim().slice(0, 200);
      warnings.push(`No se pudo parsear JSON del fallback de LicitaYa (${tenderId}). Preview: ${bodyPreview || "(vacio)"}`);
      continue;
    }

    const resultObject = payload.results;

    if (!resultObject || typeof resultObject !== "object") {
      continue;
    }

    const mapped = mapLicitayaRecord(resultObject as Record<string, unknown>, requestUrl.toString());

    if (!mapped) {
      continue;
    }

    const key = mapped.sourceRecordId ?? mapped.title;
    deduped.set(key, mapped);
  }

  if (deduped.size > 0) {
    warnings.push("LicitaYa se sincronizo via fallback de tenderId.");
    return {
      source: LicitationSource.LICITAYA,
      items: [...deduped.values()],
      rawSourceUrl,
      warnings,
    };
  }

  if (emptyDates.length > 0) {
    const firstDate = emptyDates[0];
    const lastDate = emptyDates[emptyDates.length - 1];
    warnings.push(`LicitaYa sin resultados para ${emptyDates.length} fechas consultadas (${firstDate} a ${lastDate}).`);
  }

  return {
    source: LicitationSource.LICITAYA,
    items: [],
    rawSourceUrl,
    warnings,
  };
}
