import { createHash } from "node:crypto";
import { NormativeVerificationStatus, OfficialNormativeSourceType } from "@prisma/client";
import type {
  ComplianceAlertView,
  NormativeVerificationResultView,
  OfficialNormativeSourceView,
  OfficialNormativeSuggestionInput,
} from "@/lib/compliance/types";
import { prisma } from "@/lib/prisma";

type InternalVerificationState = {
  lastKnownHash: string | null;
  lastVerifiedAt: string | null;
  nextCheckAt: string | null;
  lastStatus: "success" | "warning" | "failed" | null;
  lastMessage: string | null;
  lastChangedAt: string | null;
};

const DEFAULT_CHECK_INTERVAL_DAYS = 7;
const VERIFY_TIMEOUT_MS = 15_000;
const TEST_ENV = process.env.NODE_ENV === "test";

const DEFAULT_OFFICIAL_SOURCES: OfficialNormativeSourceView[] = [
  {
    id: "nl-ley-adquisiciones-estatal",
    stateCode: "NL",
    stateName: "Nuevo Leon",
    municipalityCode: null,
    municipalityName: null,
    authorityName: "Gobierno del Estado de Nuevo Leon",
    title: "Ley de Adquisiciones, Arrendamientos y Contratacion de Servicios del Estado de Nuevo Leon",
    officialUrl: "https://www.hcnl.gob.mx/trabajo_legislativo/leyes/",
    sourceType: "ley",
    versionLabel: null,
    lastKnownHash: null,
    lastVerifiedAt: null,
    nextCheckAt: null,
    isPilot: true,
  },
  {
    id: "nl-reglamento-adquisiciones-estatal",
    stateCode: "NL",
    stateName: "Nuevo Leon",
    municipalityCode: null,
    municipalityName: null,
    authorityName: "Gobierno del Estado de Nuevo Leon",
    title: "Reglamento de la Ley de Adquisiciones del Estado de Nuevo Leon",
    officialUrl: "https://www.nl.gob.mx/",
    sourceType: "reglamento",
    versionLabel: null,
    lastKnownHash: null,
    lastVerifiedAt: null,
    nextCheckAt: null,
    isPilot: true,
  },
  {
    id: "spgg-reglamento-adquisiciones",
    stateCode: "NL",
    stateName: "Nuevo Leon",
    municipalityCode: "SPGG",
    municipalityName: "San Pedro Garza Garcia",
    authorityName: "Municipio de San Pedro Garza Garcia",
    title: "Reglamento de Adquisiciones y Contratacion de Servicios de San Pedro Garza Garcia",
    officialUrl: "https://www.sanpedro.gob.mx/",
    sourceType: "reglamento",
    versionLabel: null,
    lastKnownHash: null,
    lastVerifiedAt: null,
    nextCheckAt: null,
    isPilot: true,
  },
];

const inMemorySuggestions: Array<OfficialNormativeSuggestionInput & { id: string; createdAt: string }> = [];
const inMemoryVerificationState = new Map<string, InternalVerificationState>();

function addDaysIso(base: Date, days: number) {
  const copy = new Date(base);
  copy.setUTCDate(copy.getUTCDate() + days);
  return copy.toISOString();
}

function normalize(value: string | null | undefined) {
  return (value ?? "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .trim();
}

function composeFingerprint(url: string, headers: Headers) {
  const etag = headers.get("etag") ?? "";
  const lastModified = headers.get("last-modified") ?? "";
  const contentLength = headers.get("content-length") ?? "";
  const contentType = headers.get("content-type") ?? "";
  return `${url}|${etag}|${lastModified}|${contentLength}|${contentType}`;
}

function sha256(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function toPrismaSourceType(value: OfficialNormativeSourceView["sourceType"]) {
  if (value === "ley") {
    return OfficialNormativeSourceType.LEY;
  }

  if (value === "reglamento") {
    return OfficialNormativeSourceType.REGLAMENTO;
  }

  if (value === "lineamiento") {
    return OfficialNormativeSourceType.LINEAMIENTO;
  }

  return OfficialNormativeSourceType.PORTAL;
}

function fromPrismaSourceType(value: OfficialNormativeSourceType): OfficialNormativeSourceView["sourceType"] {
  if (value === OfficialNormativeSourceType.LEY) {
    return "ley";
  }

  if (value === OfficialNormativeSourceType.REGLAMENTO) {
    return "reglamento";
  }

  if (value === OfficialNormativeSourceType.LINEAMIENTO) {
    return "lineamiento";
  }

  return "portal";
}

function toPrismaStatus(value: "success" | "warning" | "failed") {
  if (value === "success") {
    return NormativeVerificationStatus.SUCCESS;
  }

  if (value === "warning") {
    return NormativeVerificationStatus.WARNING;
  }

  return NormativeVerificationStatus.FAILED;
}

function fromPrismaStatus(value: NormativeVerificationStatus | null): "success" | "warning" | "failed" | null {
  if (!value) {
    return null;
  }

  if (value === NormativeVerificationStatus.SUCCESS) {
    return "success";
  }

  if (value === NormativeVerificationStatus.WARNING) {
    return "warning";
  }

  return "failed";
}

function mapSourceRowToView(row: {
  id: string;
  stateCode: string;
  stateName: string;
  municipalityCode: string | null;
  municipalityName: string | null;
  authorityName: string;
  title: string;
  officialUrl: string;
  sourceType: OfficialNormativeSourceType;
  versionLabel: string | null;
  lastKnownHash: string | null;
  lastVerifiedAt: Date | null;
  nextCheckAt: Date | null;
  isPilot: boolean;
}) {
  return {
    id: row.id,
    stateCode: row.stateCode,
    stateName: row.stateName,
    municipalityCode: row.municipalityCode,
    municipalityName: row.municipalityName,
    authorityName: row.authorityName,
    title: row.title,
    officialUrl: row.officialUrl,
    sourceType: fromPrismaSourceType(row.sourceType),
    versionLabel: row.versionLabel,
    lastKnownHash: row.lastKnownHash,
    lastVerifiedAt: row.lastVerifiedAt?.toISOString() ?? null,
    nextCheckAt: row.nextCheckAt?.toISOString() ?? null,
    isPilot: row.isPilot,
  } satisfies OfficialNormativeSourceView;
}

async function fetchSourceMetadata(url: string) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);

  try {
    const response = await fetch(url, {
      method: "GET",
      redirect: "follow",
      signal: controller.signal,
    });

    const fingerprint = composeFingerprint(response.url || url, response.headers);
    return {
      ok: response.ok,
      status: response.status,
      hash: sha256(fingerprint),
      finalUrl: response.url || url,
    };
  } finally {
    clearTimeout(timer);
  }
}

function filterSources(
  sources: OfficialNormativeSourceView[],
  filters?: {
    stateCode?: string | null;
    municipalityCode?: string | null;
    pilotOnly?: boolean;
  },
) {
  const stateCode = normalize(filters?.stateCode);
  const municipalityCode = normalize(filters?.municipalityCode);

  return sources.filter((source) => {
    if (filters?.pilotOnly && !source.isPilot) {
      return false;
    }

    if (stateCode && normalize(source.stateCode) !== stateCode) {
      return false;
    }

    if (municipalityCode && normalize(source.municipalityCode) !== municipalityCode) {
      return false;
    }

    return true;
  });
}

async function ensureDefaultSourcesPersisted() {
  await Promise.all(
    DEFAULT_OFFICIAL_SOURCES.map((source) =>
      prisma.officialNormativeSource.upsert({
        where: { id: source.id },
        update: {
          stateCode: source.stateCode,
          stateName: source.stateName,
          municipalityCode: source.municipalityCode,
          municipalityName: source.municipalityName,
          authorityName: source.authorityName,
          title: source.title,
          officialUrl: source.officialUrl,
          sourceType: toPrismaSourceType(source.sourceType),
          versionLabel: source.versionLabel,
          isPilot: source.isPilot,
        },
        create: {
          id: source.id,
          stateCode: source.stateCode,
          stateName: source.stateName,
          municipalityCode: source.municipalityCode,
          municipalityName: source.municipalityName,
          authorityName: source.authorityName,
          title: source.title,
          officialUrl: source.officialUrl,
          sourceType: toPrismaSourceType(source.sourceType),
          versionLabel: source.versionLabel,
          isPilot: source.isPilot,
        },
      }),
    ),
  );
}

function listInMemorySources(filters?: {
  stateCode?: string | null;
  municipalityCode?: string | null;
  pilotOnly?: boolean;
}) {
  const sources = DEFAULT_OFFICIAL_SOURCES.map((source) => {
    const state = inMemoryVerificationState.get(source.id);
    return {
      ...source,
      lastKnownHash: state?.lastKnownHash ?? source.lastKnownHash,
      lastVerifiedAt: state?.lastVerifiedAt ?? source.lastVerifiedAt,
      nextCheckAt: state?.nextCheckAt ?? source.nextCheckAt,
    };
  });

  return filterSources(sources, filters);
}

export async function listOfficialNormativeSources(filters?: {
  stateCode?: string | null;
  municipalityCode?: string | null;
  pilotOnly?: boolean;
}) {
  if (TEST_ENV) {
    return listInMemorySources(filters);
  }

  await ensureDefaultSourcesPersisted();
  const rows = await prisma.officialNormativeSource.findMany({
    orderBy: [{ stateCode: "asc" }, { municipalityCode: "asc" }, { title: "asc" }],
  });

  return filterSources(rows.map(mapSourceRowToView), filters);
}

export async function suggestOfficialNormativeSource(input: OfficialNormativeSuggestionInput) {
  if (TEST_ENV) {
    const suggestion = {
      ...input,
      id: `suggestion-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      createdAt: new Date().toISOString(),
    };
    inMemorySuggestions.unshift(suggestion);
    return suggestion;
  }

  const created = await prisma.officialNormativeSuggestion.create({
    data: {
      stateCode: input.stateCode,
      municipalityCode: input.municipalityCode ?? null,
      authorityName: input.authorityName,
      title: input.title,
      officialUrl: input.officialUrl,
      sourceType: toPrismaSourceType(input.sourceType),
      notes: input.notes ?? null,
    },
    select: {
      id: true,
      stateCode: true,
      municipalityCode: true,
      authorityName: true,
      title: true,
      officialUrl: true,
      sourceType: true,
      notes: true,
      createdAt: true,
    },
  });

  return {
    id: created.id,
    stateCode: created.stateCode,
    municipalityCode: created.municipalityCode,
    authorityName: created.authorityName,
    title: created.title,
    officialUrl: created.officialUrl,
    sourceType: fromPrismaSourceType(created.sourceType),
    notes: created.notes ?? undefined,
    createdAt: created.createdAt.toISOString(),
  };
}

export async function listOfficialNormativeSuggestions() {
  if (TEST_ENV) {
    return [...inMemorySuggestions];
  }

  const rows = await prisma.officialNormativeSuggestion.findMany({
    orderBy: [{ createdAt: "desc" }],
    take: 200,
    select: {
      id: true,
      stateCode: true,
      municipalityCode: true,
      authorityName: true,
      title: true,
      officialUrl: true,
      sourceType: true,
      notes: true,
      createdAt: true,
    },
  });

  return rows.map((row) => ({
    id: row.id,
    stateCode: row.stateCode,
    municipalityCode: row.municipalityCode,
    authorityName: row.authorityName,
    title: row.title,
    officialUrl: row.officialUrl,
    sourceType: fromPrismaSourceType(row.sourceType),
    notes: row.notes ?? undefined,
    createdAt: row.createdAt.toISOString(),
  }));
}

export async function verifyOfficialNormativeSources(sourceIds?: string[]) {
  const now = new Date();

  if (TEST_ENV) {
    const sources = sourceIds && sourceIds.length > 0 ? DEFAULT_OFFICIAL_SOURCES.filter((source) => sourceIds.includes(source.id)) : DEFAULT_OFFICIAL_SOURCES;
    const results: NormativeVerificationResultView[] = [];

    for (const source of sources) {
      const previous = inMemoryVerificationState.get(source.id) ?? {
        lastKnownHash: source.lastKnownHash,
        lastVerifiedAt: source.lastVerifiedAt,
        nextCheckAt: source.nextCheckAt,
        lastStatus: null,
        lastMessage: null,
        lastChangedAt: null,
      };

      try {
        const metadata = await fetchSourceMetadata(source.officialUrl);
        const changed = Boolean(previous.lastKnownHash && metadata.hash !== previous.lastKnownHash);
        const checkedAt = now.toISOString();
        const status: "success" | "warning" | "failed" = metadata.ok ? (changed ? "warning" : "success") : "failed";
        const message = metadata.ok
          ? changed
            ? "Se detecto cambio potencial en fuente normativa oficial."
            : "Fuente normativa verificada sin cambios."
          : "La fuente normativa no respondio correctamente.";

        inMemoryVerificationState.set(source.id, {
          lastKnownHash: metadata.hash,
          lastVerifiedAt: checkedAt,
          nextCheckAt: addDaysIso(now, DEFAULT_CHECK_INTERVAL_DAYS),
          lastStatus: status,
          lastMessage: message,
          lastChangedAt: changed ? checkedAt : previous.lastChangedAt,
        });

        results.push({
          sourceId: source.id,
          checkedAt,
          changed,
          status,
          httpStatus: metadata.status,
          observedHash: metadata.hash,
          message,
        });
      } catch (error) {
        const checkedAt = now.toISOString();
        const message = error instanceof Error ? error.message : "No fue posible verificar la fuente normativa.";

        inMemoryVerificationState.set(source.id, {
          lastKnownHash: previous.lastKnownHash ?? null,
          lastVerifiedAt: checkedAt,
          nextCheckAt: addDaysIso(now, 1),
          lastStatus: "failed",
          lastMessage: message,
          lastChangedAt: previous.lastChangedAt ?? null,
        });

        results.push({
          sourceId: source.id,
          checkedAt,
          changed: false,
          status: "failed",
          httpStatus: null,
          observedHash: previous.lastKnownHash ?? null,
          message,
        });
      }
    }

    return results;
  }

  await ensureDefaultSourcesPersisted();
  const sources = await prisma.officialNormativeSource.findMany({
    where: sourceIds && sourceIds.length > 0 ? { id: { in: sourceIds } } : undefined,
    orderBy: [{ title: "asc" }],
    select: {
      id: true,
      officialUrl: true,
      lastKnownHash: true,
      lastChangedAt: true,
    },
  });

  const results: NormativeVerificationResultView[] = [];

  for (const source of sources) {
    try {
      const metadata = await fetchSourceMetadata(source.officialUrl);
      const changed = Boolean(source.lastKnownHash && metadata.hash !== source.lastKnownHash);
      const checkedAt = now.toISOString();
      const status: "success" | "warning" | "failed" = metadata.ok ? (changed ? "warning" : "success") : "failed";
      const message = metadata.ok
        ? changed
          ? "Se detecto cambio potencial en fuente normativa oficial."
          : "Fuente normativa verificada sin cambios."
        : "La fuente normativa no respondio correctamente.";

      await prisma.officialNormativeSource.update({
        where: { id: source.id },
        data: {
          lastKnownHash: metadata.hash,
          lastVerifiedAt: now,
          nextCheckAt: new Date(addDaysIso(now, DEFAULT_CHECK_INTERVAL_DAYS)),
          lastStatus: toPrismaStatus(status),
          lastMessage: message,
          lastChangedAt: changed ? now : source.lastChangedAt,
        },
      });

      results.push({
        sourceId: source.id,
        checkedAt,
        changed,
        status,
        httpStatus: metadata.status,
        observedHash: metadata.hash,
        message,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "No fue posible verificar la fuente normativa.";

      await prisma.officialNormativeSource.update({
        where: { id: source.id },
        data: {
          lastVerifiedAt: now,
          nextCheckAt: new Date(addDaysIso(now, 1)),
          lastStatus: NormativeVerificationStatus.FAILED,
          lastMessage: message,
        },
      });

      results.push({
        sourceId: source.id,
        checkedAt: now.toISOString(),
        changed: false,
        status: "failed",
        httpStatus: null,
        observedHash: source.lastKnownHash,
        message,
      });
    }
  }

  return results;
}

export async function getNormativeSourcesDueForVerification(now = new Date()) {
  const nowTime = now.getTime();
  const sources = await listOfficialNormativeSources({ pilotOnly: true });

  return sources
    .filter((source) => {
      if (!source.lastVerifiedAt || !source.nextCheckAt) {
        return true;
      }

      const nextCheck = new Date(source.nextCheckAt);
      if (Number.isNaN(nextCheck.getTime())) {
        return true;
      }

      return nextCheck.getTime() <= nowTime;
    })
    .map((source) => source.id);
}

export async function runPeriodicNormativeVerification(now = new Date()) {
  const dueSourceIds = await getNormativeSourcesDueForVerification(now);

  if (dueSourceIds.length === 0) {
    return {
      checkedAt: now.toISOString(),
      checked: false,
      dueCount: 0,
      results: [] as NormativeVerificationResultView[],
    };
  }

  const results = await verifyOfficialNormativeSources(dueSourceIds);
  return {
    checkedAt: now.toISOString(),
    checked: true,
    dueCount: dueSourceIds.length,
    results,
  };
}

export async function listRegulationChangeAlerts(now = new Date(), lookbackDays = 30): Promise<ComplianceAlertView[]> {
  const threshold = new Date(now);
  threshold.setUTCDate(threshold.getUTCDate() - Math.max(1, lookbackDays));

  if (TEST_ENV) {
    const sourcesById = new Map(DEFAULT_OFFICIAL_SOURCES.map((source) => [source.id, source]));
    const alerts: ComplianceAlertView[] = [];

    for (const [sourceId, state] of inMemoryVerificationState.entries()) {
      const source = sourcesById.get(sourceId);
      if (!source || !state.lastVerifiedAt) {
        continue;
      }

      const verifiedAt = new Date(state.lastVerifiedAt);
      if (Number.isNaN(verifiedAt.getTime()) || verifiedAt < threshold) {
        continue;
      }

      if (state.lastStatus === "warning") {
        alerts.push({
          id: `reg-update-${sourceId}`,
          proposalId: null,
          licitationId: sourceId,
          title: "Cambio detectado en fuente normativa",
          description: `${source.title}: ${state.lastMessage ?? "Se detectaron cambios en metadatos oficiales."}`,
          severity: "high",
          kind: "normative_regulation_update",
          dueAt: state.lastVerifiedAt,
          createdAt: state.lastVerifiedAt,
        });
      }

      if (state.lastStatus === "failed") {
        alerts.push({
          id: `reg-failed-${sourceId}`,
          proposalId: null,
          licitationId: sourceId,
          title: "Verificacion normativa fallida",
          description: `${source.title}: ${state.lastMessage ?? "No fue posible verificar la fuente."}`,
          severity: "medium",
          kind: "normative_verification_pending",
          dueAt: state.nextCheckAt,
          createdAt: state.lastVerifiedAt,
        });
      }
    }

    return alerts;
  }

  await ensureDefaultSourcesPersisted();
  const rows = await prisma.officialNormativeSource.findMany({
    where: {
      lastVerifiedAt: { gte: threshold },
      lastStatus: { in: [NormativeVerificationStatus.WARNING, NormativeVerificationStatus.FAILED] },
    },
    orderBy: [{ lastVerifiedAt: "desc" }],
    select: {
      id: true,
      title: true,
      lastStatus: true,
      lastMessage: true,
      lastVerifiedAt: true,
      nextCheckAt: true,
    },
  });

  const alerts: ComplianceAlertView[] = [];

  for (const row of rows) {
    if (!row.lastVerifiedAt || !row.lastStatus) {
      continue;
    }

    const status = fromPrismaStatus(row.lastStatus);
    if (status === "warning") {
      alerts.push({
        id: `reg-update-${row.id}`,
        proposalId: null,
        licitationId: row.id,
        title: "Cambio detectado en fuente normativa",
        description: `${row.title}: ${row.lastMessage ?? "Se detectaron cambios en metadatos oficiales."}`,
        severity: "high",
        kind: "normative_regulation_update",
        dueAt: row.lastVerifiedAt.toISOString(),
        createdAt: row.lastVerifiedAt.toISOString(),
      });
      continue;
    }

    if (status === "failed") {
      alerts.push({
        id: `reg-failed-${row.id}`,
        proposalId: null,
        licitationId: row.id,
        title: "Verificacion normativa fallida",
        description: `${row.title}: ${row.lastMessage ?? "No fue posible verificar la fuente."}`,
        severity: "medium",
        kind: "normative_verification_pending",
        dueAt: row.nextCheckAt?.toISOString() ?? null,
        createdAt: row.lastVerifiedAt.toISOString(),
      });
    }
  }

  return alerts;
}

export async function getDueNormativeVerifications(now = new Date(), horizonDays = 7) {
  const horizon = new Date(now);
  horizon.setUTCDate(horizon.getUTCDate() + Math.max(0, horizonDays));

  const sources = await listOfficialNormativeSources({ pilotOnly: true });
  return sources
    .map((source) => {
      const dueAtIso = source.nextCheckAt ?? addDaysIso(now, 1);
      const dueAt = new Date(dueAtIso);
      if (Number.isNaN(dueAt.getTime())) {
        return null;
      }

      if (dueAt > horizon) {
        return null;
      }

      return {
        sourceId: source.id,
        sourceTitle: source.title,
        authorityName: source.authorityName,
        dueAt: dueAt.toISOString(),
        overdue: dueAt.getTime() < now.getTime(),
      };
    })
    .filter((item): item is { sourceId: string; sourceTitle: string; authorityName: string; dueAt: string; overdue: boolean } => Boolean(item))
    .sort((left, right) => new Date(left.dueAt).getTime() - new Date(right.dueAt).getTime());
}

export async function resetRegulationsStateForTests() {
  if (!TEST_ENV) {
    return;
  }

  inMemoryVerificationState.clear();
  inMemorySuggestions.splice(0, inMemorySuggestions.length);
}
