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

export const HELLOASSO_SETTINGS_KEY = "system.associationDirectory.helloasso";

export type HelloAssoSettings = {
  enabled: boolean;
  clientId: string;
  clientSecret: string;
};

export type HelloAssoSettingsPublic = {
  enabled: boolean;
  clientId: string;
  clientSecretConfigured: boolean;
};

type HelloAssoTokenResponse = {
  access_token?: string;
  token_type?: string;
  expires_in?: number;
};

type HelloAssoDirectoryItem = {
  action?: string | null;
  record?: {
    url?: string | null;
    organizationSlug?: string | null;
  } | null;
};

type HelloAssoDirectoryResponse = {
  data?: HelloAssoDirectoryItem[] | null;
  pagination?: {
    continuationToken?: string | null;
  } | null;
};

type HelloAssoOrganizationPublic = {
  facebookPage?: string | null;
  longDescription?: string | null;
  webSite?: string | null;
  address?: string | null;
  rnaNumber?: string | null;
  name?: string | null;
  city?: string | null;
  zipCode?: string | null;
  description?: string | null;
  updateDate?: string | null;
  url?: string | null;
  organizationSlug?: string | null;
};

type CandidateScore = {
  slug: string;
  detail: HelloAssoOrganizationPublic;
  score: number;
  exactRna: boolean;
  exactName: boolean;
  exactCity: boolean;
  exactZipCode: boolean;
};

export type HelloAssoSyncResult = {
  matched: boolean;
  reason: string;
  slug?: string;
  candidateCount: number;
  updates: Partial<InsertAssociationDirectoryEntry>;
};

export class HelloAssoSyncClient {
  private accessTokenPromise: Promise<string> | null = null;

  constructor(private readonly settings: HelloAssoSettings) {}

  async getAccessToken() {
    if (!this.accessTokenPromise) {
      this.accessTokenPromise = fetchHelloAssoAccessToken(this.settings);
    }
    return this.accessTokenPromise;
  }
}

export function sanitizeHelloAssoSettings(rawValue?: unknown): HelloAssoSettings {
  const source = rawValue && typeof rawValue === "object" ? rawValue as Record<string, unknown> : {};
  return {
    enabled: source.enabled !== false,
    clientId: typeof source.clientId === "string" ? source.clientId.trim() : "",
    clientSecret: typeof source.clientSecret === "string" ? source.clientSecret.trim() : "",
  };
}

export function serializeHelloAssoSettingsPublic(settings: HelloAssoSettings): HelloAssoSettingsPublic {
  return {
    enabled: settings.enabled,
    clientId: settings.clientId,
    clientSecretConfigured: settings.clientSecret.length > 0,
  };
}

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 normalizeZipCode(value: string | null | undefined) {
  const digits = cleanDigits(value);
  return digits.length >= 5 ? digits.slice(0, 5) : null;
}

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

function normalizeOptionalUrl(value: string | null | undefined) {
  const trimmed = String(value || "").trim();
  if (!trimmed) return null;
  if (/^https?:\/\//i.test(trimmed)) return trimmed;
  return `https://${trimmed}`;
}

function normalizeOptionalText(value: string | null | undefined) {
  const trimmed = String(value || "").trim();
  return trimmed || null;
}

function buildSearchBodies(entry: AssociationDirectoryEntry, association?: Association | null) {
  const name = normalizeOptionalText(association?.nomAssociation || entry.nomAssociation);
  const city = normalizeOptionalText(association?.ville || entry.ville);
  const zipCode = normalizeZipCode(association?.codePostal || entry.codePostal);

  const variants = [
    {
      name,
      ...(city ? { cities: [city] } : {}),
      ...(zipCode ? { zipCodes: [zipCode] } : {}),
    },
    {
      name,
      ...(city ? { cities: [city] } : {}),
    },
    {
      name,
      ...(zipCode ? { zipCodes: [zipCode] } : {}),
    },
    {
      name,
    },
  ];

  const seen = new Set<string>();
  return variants.filter((variant) => {
    if (!variant.name) return false;
    const key = JSON.stringify(variant);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

async function fetchHelloAssoJson<T>(path: string, token: string, init?: RequestInit) {
  const response = await fetch(`https://api.helloasso.com/v5${path}`, {
    ...init,
    headers: {
      Accept: "application/json",
      Authorization: `Bearer ${token}`,
      ...(init?.headers || {}),
    },
  });

  if (!response.ok) {
    const body = await response.text().catch(() => "");
    throw new Error(`HelloAsso HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
  }

  return await response.json() as T;
}

async function fetchHelloAssoAccessToken(settings: HelloAssoSettings) {
  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: settings.clientId,
    client_secret: settings.clientSecret,
  });

  const response = await fetch("https://api.helloasso.com/oauth2/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Accept: "application/json",
    },
    body: body.toString(),
  });

  if (!response.ok) {
    const raw = await response.text().catch(() => "");
    throw new Error(
      response.status === 401 || response.status === 403
        ? "Identifiants HelloAsso invalides ou non autorisés"
        : `Impossible d'obtenir un jeton HelloAsso (${response.status})${raw ? `: ${raw.slice(0, 160)}` : ""}`
    );
  }

  const payload = await response.json() as HelloAssoTokenResponse;
  if (!payload.access_token) {
    throw new Error("HelloAsso n'a pas renvoyé de jeton d'accès exploitable");
  }

  return payload.access_token;
}

async function searchHelloAssoDirectory(
  token: string,
  entry: AssociationDirectoryEntry,
  association?: Association | null
) {
  const slugs = new Set<string>();

  for (const body of buildSearchBodies(entry, association)) {
    try {
      const response = await fetchHelloAssoJson<HelloAssoDirectoryResponse>("/directory/organizations?pageSize=8", token, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });

      for (const item of response.data || []) {
        const slug = item.record?.organizationSlug?.trim();
        if (!slug) continue;
        if ((item.action || "").toLowerCase() === "delete") continue;
        slugs.add(slug);
      }

      if (slugs.size > 0) {
        break;
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : "Erreur inconnue";
      if (message.includes("403")) {
        throw new Error("Le client HelloAsso doit disposer du privilège OrganizationOpenDirectory pour interroger le répertoire.");
      }
      throw error;
    }
  }

  return Array.from(slugs);
}

function scoreHelloAssoCandidate(
  entry: AssociationDirectoryEntry,
  association: Association | null | undefined,
  detail: HelloAssoOrganizationPublic
): CandidateScore {
  const localName = normalizeText(association?.nomAssociation || entry.nomAssociation);
  const localCity = normalizeText(association?.ville || entry.ville);
  const localZipCode = normalizeZipCode(association?.codePostal || entry.codePostal);
  const localRna = normalizeRna(association?.rna || entry.rna);

  const remoteName = normalizeText(detail.name);
  const remoteCity = normalizeText(detail.city);
  const remoteZipCode = normalizeZipCode(detail.zipCode);
  const remoteRna = normalizeRna(detail.rnaNumber);

  const exactRna = Boolean(localRna && remoteRna && localRna === remoteRna);
  const exactName = Boolean(localName && remoteName && localName === remoteName);
  const exactCity = Boolean(localCity && remoteCity && localCity === remoteCity);
  const exactZipCode = Boolean(localZipCode && remoteZipCode && localZipCode === remoteZipCode);

  let score = 0;
  if (exactRna) score += 200;
  if (exactName) score += 90;
  else if (remoteName && (remoteName.includes(localName) || localName.includes(remoteName))) score += 35;
  if (exactCity) score += 20;
  if (exactZipCode) score += 20;
  if (normalizeOptionalUrl(detail.webSite) && normalizeOptionalUrl(detail.webSite) === normalizeOptionalUrl(association?.siteWeb || entry.siteWeb)) {
    score += 30;
  }

  return {
    slug: detail.organizationSlug || "",
    detail,
    score,
    exactRna,
    exactName,
    exactCity,
    exactZipCode,
  };
}

function pickHelloAssoCandidate(
  entry: AssociationDirectoryEntry,
  association: Association | null | undefined,
  details: HelloAssoOrganizationPublic[]
) {
  const scored = details
    .filter((detail) => Boolean(detail.organizationSlug))
    .map((detail) => scoreHelloAssoCandidate(entry, association, detail))
    .sort((a, b) => b.score - a.score);

  if (scored.length === 0) return null;

  const exactRna = scored.filter((candidate) => candidate.exactRna);
  if (exactRna.length === 1) return exactRna[0];

  const exactNameAndLocation = scored.filter((candidate) => candidate.exactName && (candidate.exactCity || candidate.exactZipCode));
  if (exactNameAndLocation.length === 1) return exactNameAndLocation[0];

  const exactNameOnly = scored.filter((candidate) => candidate.exactName);
  if (exactNameOnly.length === 1) return exactNameOnly[0];

  const [best, second] = scored;
  if (best && best.score >= 120 && (!second || best.score - second.score >= 20)) {
    return best;
  }

  return null;
}

function buildHelloAssoUpdates(detail: HelloAssoOrganizationPublic): Partial<InsertAssociationDirectoryEntry> {
  const description = normalizeOptionalText(detail.longDescription) || normalizeOptionalText(detail.description);
  const address = normalizeOptionalText(detail.address);
  const city = normalizeOptionalText(detail.city);
  const zipCode = normalizeZipCode(detail.zipCode);
  const webSite = normalizeOptionalUrl(detail.webSite);
  const facebookPage = normalizeOptionalUrl(detail.facebookPage);
  const rna = normalizeRna(detail.rnaNumber);
  const updateDate = parseDate(detail.updateDate);
  const slug = normalizeOptionalText(detail.organizationSlug);

  return {
    ...(rna ? { rna } : {}),
    ...(address ? { adresse: address } : {}),
    ...(city ? { ville: city } : {}),
    ...(zipCode ? { codePostal: zipCode } : {}),
    ...(webSite ? { siteWeb: webSite } : {}),
    ...(facebookPage ? { facebookUrl: facebookPage } : {}),
    ...(description ? { objetAssociation: description } : {}),
    externalSourceStatus: "helloasso_synced",
    externalSourceLabel: slug ? `HelloAsso · ${slug}` : "HelloAsso",
    ...(updateDate ? { registryLastUpdatedAt: updateDate } : {}),
  };
}

export async function computeAssociationDirectoryHelloAssoUpdate(
  entry: AssociationDirectoryEntry,
  association: Association | null | undefined,
  settings: HelloAssoSettings,
  client?: HelloAssoSyncClient
): Promise<HelloAssoSyncResult> {
  if (!settings.enabled) {
    return {
      matched: false,
      reason: "La synchronisation HelloAsso est désactivée.",
      candidateCount: 0,
      updates: {
        externalSourceStatus: "helloasso_disabled",
        externalSourceLabel: "HelloAsso",
      },
    };
  }

  if (!settings.clientId || !settings.clientSecret) {
    return {
      matched: false,
      reason: "Les identifiants HelloAsso ne sont pas configurés.",
      candidateCount: 0,
      updates: {
        externalSourceStatus: "helloasso_not_configured",
        externalSourceLabel: "HelloAsso",
      },
    };
  }

  const syncClient = client || new HelloAssoSyncClient(settings);
  const token = await syncClient.getAccessToken();
  const slugs = await searchHelloAssoDirectory(token, entry, association);

  if (slugs.length === 0) {
    return {
      matched: false,
      reason: "Aucun organisme HelloAsso compatible n'a été trouvé pour cette association.",
      candidateCount: 0,
      updates: {
        externalSourceStatus: "helloasso_no_match",
        externalSourceLabel: "HelloAsso",
      },
    };
  }

  const details = await Promise.all(
    slugs.map(async (slug) => {
      try {
        return await fetchHelloAssoJson<HelloAssoOrganizationPublic>(`/organizations/${encodeURIComponent(slug)}`, token);
      } catch {
        return null;
      }
    })
  );

  const matched = pickHelloAssoCandidate(entry, association, details.filter(Boolean) as HelloAssoOrganizationPublic[]);

  if (!matched) {
    return {
      matched: false,
      reason: "Des résultats HelloAsso ont été trouvés, mais aucun rapprochement n'est assez fiable pour mettre à jour la fiche automatiquement.",
      candidateCount: details.filter(Boolean).length,
      updates: {
        externalSourceStatus: "helloasso_ambiguous_match",
        externalSourceLabel: "HelloAsso",
      },
    };
  }

  return {
    matched: true,
    reason: matched.exactRna
      ? "Correspondance HelloAsso validée par le RNA."
      : matched.exactName && (matched.exactCity || matched.exactZipCode)
        ? "Correspondance HelloAsso validée par le nom et la localisation."
        : "Correspondance HelloAsso validée par le nom de l'association.",
    slug: matched.slug,
    candidateCount: details.filter(Boolean).length,
    updates: buildHelloAssoUpdates(matched.detail),
  };
}
