import type { Association, AssociationDirectoryEntry, InsertAssociationDirectoryEntry } from "../drizzle/schema";
import { ENV } from "./_core/env";

export type ReferenceProposalChange = {
  field: string;
  label: string;
  currentValue: string;
  proposedValue: string;
};

export type ReferenceProposalResult = {
  hasChanges: boolean;
  updates: Partial<InsertAssociationDirectoryEntry>;
  changes: ReferenceProposalChange[];
  sourceLabel: string | null;
  summary: string;
};

type DjepvaAssociationResponse = {
  data?: {
    association?: {
      rna?: string | null;
      siret_siege?: string | null;
      active?: boolean | null;
      date_creation?: string | null;
      objet?: string | null;
      adresse_siege?: {
        code_postal?: string | null;
        commune?: string | null;
        numero_voie?: string | null;
        type_voie?: string | null;
        libelle_voie?: string | null;
      } | null;
      forme_juridique?: {
        libelle?: string | null;
      } | null;
      reconnue_utilite_publique?: boolean | null;
    } | null;
    meta?: {
      date_derniere_mise_a_jour_sirene?: string | null;
      date_derniere_mise_a_jour_rna?: string | null;
    } | null;
  } | null;
};

type SearchApiResult = {
  siren?: string | null;
  nom_complet?: string | null;
  nom_raison_sociale?: string | null;
  date_creation?: string | null;
  date_mise_a_jour?: string | null;
  date_mise_a_jour_insee?: string | null;
  etat_administratif?: string | null;
  nature_juridique?: string | null;
  siege?: {
    siret?: string | null;
    adresse?: string | null;
    code_postal?: string | null;
    libelle_commune?: string | null;
  } | null;
  complements?: {
    identifiant_association?: string | null;
    est_association?: boolean | null;
  } | null;
};

type SearchApiResponse = {
  results?: SearchApiResult[];
};

function normalizeText(value: string | null | undefined) {
  return (value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9]+/g, " ")
    .trim()
    .toLowerCase();
}

function cleanDigits(value: string | null | undefined) {
  return (value || "").replace(/\D/g, "");
}

function normalizeRna(value: string | null | undefined) {
  const trimmed = (value || "").trim().toUpperCase();
  return /^W\d{9}$/.test(trimmed) ? trimmed : null;
}

function normalizeSiret(value: string | null | undefined) {
  const digits = cleanDigits(value);
  return digits.length === 14 ? digits : null;
}

function normalizeSiren(value: string | null | undefined) {
  const digits = cleanDigits(value);
  return digits.length === 9 ? digits : null;
}

function parseDate(value: string | null | undefined) {
  if (!value) return null;
  const parsed = new Date(value);
  return Number.isNaN(parsed.getTime()) ? null : parsed;
}

function maxDate(...values: Array<Date | null>) {
  const dates = values.filter((value): value is Date => Boolean(value));
  if (dates.length === 0) return null;
  return new Date(Math.max(...dates.map((date) => date.getTime())));
}

function buildAddressLabel(parts: Array<string | null | undefined>) {
  const cleaned = parts.map((part) => (part || "").trim()).filter(Boolean);
  return cleaned.length > 0 ? cleaned.join(" ") : null;
}

function mapAssociationStatus(active: boolean | null | undefined, etatAdministratif?: string | null) {
  if (typeof active === "boolean") {
    return active ? "active" : "inactive";
  }
  if (etatAdministratif === "A") return "active";
  if (etatAdministratif === "C" || etatAdministratif === "F") return "inactive";
  return null;
}

function mapStatutJuridique(libelle?: string | null, reconnueUtilitePublique?: boolean | null) {
  const normalized = normalizeText(libelle);
  if (reconnueUtilitePublique) {
    return "association_reconnue_utilite_publique" as const;
  }
  if (normalized.includes("fondation")) {
    return "fondation" as const;
  }
  if (normalized.includes("association")) {
    return "association_loi_1901" as const;
  }
  return null;
}

async function fetchJson<T>(url: URL | string, init?: RequestInit) {
  const response = await fetch(url, init);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return await response.json() as T;
}

async function fetchDjepvaAssociation(identifier: string) {
  if (!ENV.entrepriseApiToken) {
    return null;
  }

  const url = `https://entreprise.api.gouv.fr/v4/djepva/api-association/associations/${encodeURIComponent(identifier)}`;
  try {
    return await fetchJson<DjepvaAssociationResponse>(url, {
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${ENV.entrepriseApiToken}`,
        "User-Agent": "portail-associations/1.0",
      },
    });
  } catch {
    return null;
  }
}

function scoreSearchResult(result: SearchApiResult, targetName: string, targetCity?: string | null) {
  let score = 0;
  const resultName = normalizeText(result.nom_raison_sociale || result.nom_complet);
  const normalizedTargetCity = normalizeText(targetCity);
  const resultCity = normalizeText(result.siege?.libelle_commune);

  if (result.complements?.est_association) score += 10;
  if (resultName === targetName) score += 100;
  else if (resultName.includes(targetName) || targetName.includes(resultName)) score += 40;
  if (normalizedTargetCity && resultCity && normalizedTargetCity === resultCity) score += 20;
  if (result.siege?.siret) score += 5;
  return score;
}

async function searchAssociationInSirene(entry: AssociationDirectoryEntry, association?: Association | null) {
  const knownSiret = normalizeSiret(association?.siret || entry.siret);
  const knownSiren = normalizeSiren(knownSiret ? knownSiret.slice(0, 9) : association?.siret || entry.siret);
  const query = knownSiret || knownSiren || association?.nomAssociation || entry.nomAssociation;
  if (!query) return null;

  const url = new URL("https://recherche-entreprises.api.gouv.fr/search");
  url.searchParams.set("q", query);
  url.searchParams.set("page", "1");
  url.searchParams.set("per_page", "10");
  url.searchParams.set("est_association", "true");
  if (!knownSiret && !knownSiren) {
    const codePostal = association?.codePostal || entry.codePostal;
    if (codePostal) {
      url.searchParams.set("code_postal", codePostal);
    }
  }

  try {
    const payload = await fetchJson<SearchApiResponse>(url, {
      headers: {
        Accept: "application/json",
        "User-Agent": "portail-associations/1.0",
      },
    });

    const results = payload.results || [];
    if (results.length === 0) return null;

    if (knownSiret) {
      return results.find((result) => normalizeSiret(result.siege?.siret) === knownSiret) || results[0];
    }
    if (knownSiren) {
      return results.find((result) => normalizeSiren(result.siren) === knownSiren) || results[0];
    }

    const targetName = normalizeText(association?.nomAssociation || entry.nomAssociation);
    const targetCity = association?.ville || entry.ville;
    return [...results].sort((a, b) => scoreSearchResult(b, targetName, targetCity) - scoreSearchResult(a, targetName, targetCity))[0];
  } catch {
    return null;
  }
}

export async function computeAssociationDirectoryReferenceUpdate(
  entry: AssociationDirectoryEntry,
  association?: Association | null
) {
  const knownRna = normalizeRna(association?.rna || entry.rna);
  const knownSiret = normalizeSiret(association?.siret || entry.siret);
  const knownSiren = normalizeSiren(knownSiret ? knownSiret.slice(0, 9) : association?.siret || entry.siret);
  const djepvaIdentifier = knownRna || knownSiren;

  const [djepva, searchResult] = await Promise.all([
    djepvaIdentifier ? fetchDjepvaAssociation(djepvaIdentifier) : Promise.resolve(null),
    searchAssociationInSirene(entry, association),
  ]);

  const djepvaAssociation = djepva?.data?.association || null;
  const djepvaMeta = djepva?.data?.meta || null;

  const resolvedRna =
    normalizeRna(djepvaAssociation?.rna) ||
    normalizeRna(searchResult?.complements?.identifiant_association) ||
    knownRna;
  const resolvedSiret =
    normalizeSiret(djepvaAssociation?.siret_siege) ||
    normalizeSiret(searchResult?.siege?.siret) ||
    knownSiret;
  const resolvedStatus =
    mapAssociationStatus(djepvaAssociation?.active, searchResult?.etat_administratif) ||
    entry.associationStatus ||
    null;

  const registryLastUpdatedAt = maxDate(
    parseDate(djepvaMeta?.date_derniere_mise_a_jour_rna),
    parseDate(djepvaMeta?.date_derniere_mise_a_jour_sirene),
    parseDate(searchResult?.date_mise_a_jour_insee),
    parseDate(searchResult?.date_mise_a_jour)
  );

  const inferredDateCreation =
    parseDate(djepvaAssociation?.date_creation) ||
    parseDate(searchResult?.date_creation) ||
    entry.dateCreation ||
    null;

  const inferredAddress =
    buildAddressLabel([
      djepvaAssociation?.adresse_siege?.numero_voie,
      djepvaAssociation?.adresse_siege?.type_voie,
      djepvaAssociation?.adresse_siege?.libelle_voie,
    ]) ||
    searchResult?.siege?.adresse ||
    entry.adresse ||
    null;

  const inferredCodePostal =
    djepvaAssociation?.adresse_siege?.code_postal ||
    searchResult?.siege?.code_postal ||
    entry.codePostal ||
    null;

  const inferredVille =
    djepvaAssociation?.adresse_siege?.commune ||
    searchResult?.siege?.libelle_commune ||
    entry.ville ||
    null;

  const resolvedStatutJuridique =
    mapStatutJuridique(djepvaAssociation?.forme_juridique?.libelle, djepvaAssociation?.reconnue_utilite_publique) ||
    entry.statutJuridique ||
    null;

  const sourceLabels = [
    djepvaAssociation ? "API RNA (DJEPVA)" : null,
    searchResult ? "API SIRENE / Recherche d’entreprises" : null,
  ].filter(Boolean);

  const hasAnyReference = Boolean(resolvedRna || resolvedSiret || resolvedStatus || registryLastUpdatedAt);

  const updates: Partial<InsertAssociationDirectoryEntry> = {
    rna: resolvedRna,
    siret: resolvedSiret,
    associationStatus: resolvedStatus,
    registryLastUpdatedAt,
    referenceLastCheckedAt: new Date(),
    referenceStatus: hasAnyReference ? "reference_data_synced" : "reference_data_unresolved",
    referenceSourceLabel: sourceLabels.length > 0 ? sourceLabels.join(" + ") : null,
    dateCreation: inferredDateCreation,
    adresse: inferredAddress,
    codePostal: inferredCodePostal,
    ville: inferredVille,
  };

  if (resolvedStatutJuridique) {
    updates.statutJuridique = resolvedStatutJuridique;
  }
  if (djepvaAssociation?.objet && !entry.objetAssociation) {
    updates.objetAssociation = djepvaAssociation.objet;
  }

  return updates;
}

const proposalFieldLabels: Record<string, string> = {
  rna: "RNA",
  siret: "SIRET",
  associationStatus: "Statut",
  dateCreation: "Date de création",
  adresse: "Adresse",
  codePostal: "Code postal",
  ville: "Ville",
  statutJuridique: "Statut juridique",
  objetAssociation: "Objet de l'association",
};

function normalizeComparableDate(value: unknown) {
  if (!value) return "";
  const date = value instanceof Date ? value : new Date(String(value));
  if (Number.isNaN(date.getTime())) return "";
  return date.toISOString().slice(0, 10);
}

function normalizeComparableValue(field: string, value: unknown) {
  if (value == null) return "";
  if (field.toLowerCase().includes("date")) {
    return normalizeComparableDate(value);
  }
  if (typeof value === "string") {
    return value.trim();
  }
  return String(value);
}

function formatProposalValue(field: string, value: unknown) {
  if (value == null || value === "") return "Non renseigné";
  if (field.toLowerCase().includes("date")) {
    const normalized = normalizeComparableDate(value);
    return normalized || "Non renseigné";
  }
  return String(value);
}

export async function computeAssociationDirectoryReferenceProposal(
  entry: AssociationDirectoryEntry,
  association?: Association | null
): Promise<ReferenceProposalResult> {
  const updates = await computeAssociationDirectoryReferenceUpdate(entry, association);
  const proposalFields = Object.keys(proposalFieldLabels);

  const changes: ReferenceProposalChange[] = proposalFields
    .filter((field) => field in updates)
    .map((field) => {
      const currentValue = normalizeComparableValue(field, (entry as Record<string, unknown>)[field]);
      const proposedValue = normalizeComparableValue(field, (updates as Record<string, unknown>)[field]);
      if (!proposedValue || currentValue === proposedValue) {
        return null;
      }
      return {
        field,
        label: proposalFieldLabels[field],
        currentValue: formatProposalValue(field, (entry as Record<string, unknown>)[field]),
        proposedValue: formatProposalValue(field, (updates as Record<string, unknown>)[field]),
      };
    })
    .filter(Boolean) as ReferenceProposalChange[];

  const sourceLabel = updates.referenceSourceLabel || null;
  const summary = changes.length
    ? `${changes.length} champ(s) à revoir : ${changes.slice(0, 4).map((change) => change.label).join(", ")}${changes.length > 4 ? "…" : ""}`
    : sourceLabel
      ? `Aucune différence utile détectée malgré une lecture via ${sourceLabel}.`
      : "Aucune donnée de référence exploitable n'a été trouvée.";

  return {
    hasChanges: changes.length > 0,
    updates,
    changes,
    sourceLabel,
    summary,
  };
}
