import { randomBytes } from "node:crypto";
import * as db from "./db";
import { sendOperationalEmail } from "./mailer";
import { generateMaterialReturnReminderEmail } from "./materialReturnEmail";
import { generateMaterialReturnStatementPdf } from "./materialReturnPdf";
import { getLogisticsSettings } from "./logisticsSettings";
import { sanitizeMaterialEventQuantityMap } from "@shared/materialEvent";

type RequestLike = Awaited<ReturnType<typeof db.getRequestById>>;

export const materialReturnStatusLabels = {
  planifie: "Planifié",
  en_attente: "En attente",
  en_cours: "En cours",
  cloture: "Clôturé / Récupéré",
} as const;

export const materialReturnStatusColors = {
  planifie: "bg-slate-100 text-slate-700",
  en_attente: "bg-red-100 text-red-700",
  en_cours: "bg-amber-100 text-amber-700",
  cloture: "bg-green-100 text-green-700",
} as const;

export const materialReturnBoardStateLabels = {
  a_attribuer: "À attribuer",
  planifie: "Planifié",
  terrain: "Sur le terrain",
  retard: "Alerte / retard",
  conforme: "Retour conforme",
  litige: "Litige / dégradation",
} as const;

export const materialReturnBoardStateColors = {
  a_attribuer: "bg-slate-100 text-slate-700 border-slate-200",
  planifie: "bg-blue-100 text-blue-700 border-blue-200",
  terrain: "bg-amber-100 text-amber-700 border-amber-200",
  retard: "bg-red-100 text-red-700 border-red-200",
  conforme: "bg-green-100 text-green-700 border-green-200",
  litige: "bg-orange-100 text-orange-800 border-orange-200",
} as const;

export const materialReturnDiscrepancyOptions = [
  { value: "materiel_manquant", label: "Matériel manquant" },
  { value: "accessoires_manquants", label: "Visserie / accessoires manquants" },
  { value: "structure_deformee", label: "Structure déformée / tordue" },
  { value: "toile_dechiree", label: "Toile / bâche déchirée" },
  { value: "choc_important", label: "Choc important" },
  { value: "materiel_sale", label: "Matériel restitué sale" },
  { value: "materiel_humide", label: "Matériel humide (risque moisissure)" },
] as const;

export const agentRoleOptions = [
  "Agent technique",
  "Responsable logistique",
  "Agent MJS",
  "Agent DSU",
] as const;

export const borrowerRoleOptions = [
  "Président d'association",
  "Trésorier",
  "Régisseur commune",
  "Bénévole mandaté",
] as const;

export function parseRecipientEmails(value: string | null | undefined) {
  if (!value) return [] as string[];
  try {
    const parsed = JSON.parse(value);
    if (!Array.isArray(parsed)) return [];
    return parsed.map((entry) => String(entry || "").trim()).filter(Boolean);
  } catch {
    return [];
  }
}

export function formatMaterialReturnDate(date: Date | string | null | undefined) {
  if (!date) return "";
  return new Date(date).toISOString().split("T")[0];
}

export function buildMaterialReturnUploadLink(baseUrl: string, token: string) {
  return `${baseUrl.replace(/\/$/, "")}/materiel/restitution/${token}`;
}

export function getMaterialReturnUploadLinkState(input: {
  status?: string | null;
  uploadTokenExpiresAt?: Date | string | null;
  now?: Date;
}) {
  if (input.status === "cloture") {
    return "closed" as const;
  }
  if (!input.uploadTokenExpiresAt) {
    return "expired" as const;
  }
  const now = input.now || new Date();
  return now <= new Date(input.uploadTokenExpiresAt) ? "active" as const : "expired" as const;
}

function getRestitutionDateFromRequest(request: NonNullable<RequestLike>) {
  try {
    const formData = request.formData ? JSON.parse(request.formData) : {};
    const rawDate = formData?.dateRestitution;
    if (!rawDate) return null;
    const date = new Date(rawDate);
    return Number.isNaN(date.getTime()) ? null : date;
  } catch {
    return null;
  }
}

function getPlannedSendAt(restitutionDate: Date, leadDays: number) {
  const planned = new Date(restitutionDate);
  planned.setDate(planned.getDate() - leadDays);
  planned.setHours(8, 0, 0, 0);
  return planned;
}

function getUploadTokenExpiry(restitutionDate: Date, graceDays: number) {
  const expiry = new Date(restitutionDate);
  expiry.setDate(expiry.getDate() + graceDays);
  expiry.setHours(23, 59, 59, 999);
  return expiry;
}

function getReactivatedUploadTokenExpiry(now: Date, graceDays: number) {
  const expiry = new Date(now);
  expiry.setDate(expiry.getDate() + Math.max(0, graceDays));
  expiry.setHours(23, 59, 59, 999);
  return expiry;
}

function getDelayAlertDate(restitutionDate: Date | string, graceDays: number) {
  const alertDate = new Date(restitutionDate);
  alertDate.setDate(alertDate.getDate() + graceDays);
  alertDate.setHours(0, 0, 0, 0);
  return alertDate;
}

function hasOperationalAssignment(followup: {
  serviceLabel?: string | null;
  recipientEmails?: string | null;
} | null | undefined) {
  if (!followup) return false;
  return Boolean(followup.serviceLabel?.trim()) || parseRecipientEmails(followup.recipientEmails).length > 0;
}

function mergeRecipientLists(...recipientSets: Array<string[] | undefined>) {
  return Array.from(
    new Set(
      recipientSets
        .flatMap((recipientSet) => recipientSet || [])
        .map((entry) => String(entry || "").trim())
        .filter(Boolean)
    )
  );
}

function getNotificationRecipients(followup: {
  recipientEmails?: string | null;
  supervisionRecipientEmails?: string | null;
}, tone: "assignment" | "scheduled" | "overdue" | "reactivation") {
  const terrainRecipients = parseRecipientEmails(followup.recipientEmails);
  const supervisionRecipients = parseRecipientEmails(followup.supervisionRecipientEmails);
  if (tone === "overdue") {
    return mergeRecipientLists(supervisionRecipients, terrainRecipients);
  }
  return terrainRecipients;
}

export function computeMaterialReturnBoardState(input: {
  requestStatus?: string | null;
  restitutionDate?: Date | string | null;
  followup?: {
    status?: string | null;
    issueFlag?: boolean | null;
    compliance?: string | null;
    litigationStatus?: string | null;
    serviceLabel?: string | null;
    recipientEmails?: string | null;
  } | null;
  now?: Date;
  graceDays?: number;
}) {
  const now = input.now || new Date();
  const followup = input.followup;
  const graceDays = Number.isFinite(input.graceDays) ? Math.max(0, input.graceDays || 0) : 3;

  if (followup?.litigationStatus === "pending") {
    return "litige" as const;
  }
  if (followup?.status === "cloture") {
    return "conforme" as const;
  }

  const restitutionDate = input.restitutionDate ? new Date(input.restitutionDate) : null;
  const assigned = hasOperationalAssignment(followup);
  if (!assigned) {
    return "a_attribuer" as const;
  }

  if (restitutionDate) {
    const delayAlertDate = getDelayAlertDate(restitutionDate, graceDays);
    if (now >= delayAlertDate) {
      return "retard" as const;
    }

    const restitutionDay = new Date(restitutionDate);
    restitutionDay.setHours(0, 0, 0, 0);
    const currentDay = new Date(now);
    currentDay.setHours(0, 0, 0, 0);
    if (currentDay >= restitutionDay) {
      return "terrain" as const;
    }
  }

  return "planifie" as const;
}

export async function sendMaterialReturnFollowupEmail(input: {
  followup: NonNullable<Awaited<ReturnType<typeof db.getMaterialReturnFollowupByRequestId>>>;
  request: NonNullable<RequestLike>;
  association: Awaited<ReturnType<typeof db.getAssociationById>>;
  baseUrl: string;
  tone?: "assignment" | "scheduled" | "overdue" | "reactivation";
}) {
  const tone = input.tone || "scheduled";
  const recipients = getNotificationRecipients(input.followup, tone);
  if (recipients.length === 0) {
    return { sent: false as const, reason: "no_recipients" as const };
  }

  const uploadLink = buildMaterialReturnUploadLink(input.baseUrl, input.followup.uploadToken);
  const pdf = await generateMaterialReturnStatementPdf({
    request: input.request,
    association: input.association,
  });
  const email = generateMaterialReturnReminderEmail({
    associationName: input.association?.nomAssociation || "Association",
    serviceLabel: input.followup.serviceLabel,
    restitutionDate: input.followup.restitutionDate,
    uploadLink,
    requestTitle: input.request.titre,
    tone,
  });

  return sendOperationalEmail({
    to: recipients,
    subject: email.subject,
    text: email.text,
    html: email.html,
    attachments: [
      {
        filename: pdf.fileName,
        content: pdf.buffer,
        contentType: "application/pdf",
      },
    ],
  });
}

export async function scheduleMaterialReturnFollowup(input: {
  request: NonNullable<RequestLike>;
  serviceLabel?: string;
  recipientEmails: string[];
  supervisionServiceLabel?: string;
  supervisionRecipientEmails?: string[];
}, options?: {
  forceNewToken?: boolean;
  forceImmediateSendWindow?: boolean;
  reactivatedByUserId?: number | null;
  reactivatedAt?: Date | null;
  reactivationReason?: string | null;
}) {
  const logisticsSettings = await getLogisticsSettings();
  const restitutionDate = getRestitutionDateFromRequest(input.request);
  if (!restitutionDate) return null;

  const existing = await db.getMaterialReturnFollowupByRequestId(input.request.id);
  const uploadToken = options?.forceNewToken || !existing?.uploadToken
    ? randomBytes(32).toString("hex")
    : existing.uploadToken;
  const now = new Date();
  const plannedSendAt = options?.forceImmediateSendWindow ? now : getPlannedSendAt(restitutionDate, logisticsSettings.materialReturnLeadDays);
  const defaultUploadTokenExpiry = getUploadTokenExpiry(restitutionDate, logisticsSettings.materialReturnGraceDays);
  const shouldReactivateWindow = Boolean(options?.forceNewToken || options?.forceImmediateSendWindow);
  const uploadTokenExpiresAt = shouldReactivateWindow
    ? getReactivatedUploadTokenExpiry(options?.reactivatedAt || now, logisticsSettings.materialReturnGraceDays)
    : existing
      && existing.status !== "cloture"
      && existing.uploadTokenExpiresAt
      && new Date(existing.uploadTokenExpiresAt) < now
        ? getReactivatedUploadTokenExpiry(now, logisticsSettings.materialReturnGraceDays)
        : defaultUploadTokenExpiry;

  await db.upsertMaterialReturnFollowup(input.request.id, {
    associationId: input.request.associationId,
    serviceLabel: input.serviceLabel || null,
    recipientEmails: JSON.stringify(input.recipientEmails),
    supervisionServiceLabel: input.supervisionServiceLabel || null,
    supervisionRecipientEmails: JSON.stringify(input.supervisionRecipientEmails || []),
    restitutionDate,
    plannedSendAt,
    status: existing?.status === "cloture" ? "cloture" : "planifie",
    uploadToken,
    uploadTokenExpiresAt,
    signedFileKey: existing?.signedFileKey || null,
    signedFileUrl: existing?.signedFileUrl || null,
    signedFileName: existing?.signedFileName || null,
    signedMimeType: existing?.signedMimeType || null,
    uploadedByName: existing?.uploadedByName || null,
    uploadedByEmail: existing?.uploadedByEmail || null,
    uploadedAt: existing?.uploadedAt || null,
    closedAt: existing?.closedAt || null,
    lastReminderSentAt: existing?.lastReminderSentAt || null,
    reactivatedByUserId: options?.reactivatedByUserId ?? existing?.reactivatedByUserId ?? null,
    reactivatedAt: options?.reactivatedAt ?? existing?.reactivatedAt ?? null,
    reactivationReason: options?.reactivationReason ?? existing?.reactivationReason ?? null,
    sentAt: existing?.sentAt || null,
  });

  return db.getMaterialReturnFollowupByRequestId(input.request.id);
}

export async function runMaterialReturnScheduler(baseUrl: string) {
  const now = new Date();
  const logisticsSettings = await getLogisticsSettings();

  const toEscalate = await db.listMaterialReturnFollowupsToEscalate();
  for (const followup of toEscalate) {
    const alertDate = getDelayAlertDate(followup.restitutionDate, logisticsSettings.materialReturnGraceDays);
    if (now >= alertDate) {
      const shouldSendOverdueAlert = !followup.lastReminderSentAt || new Date(followup.lastReminderSentAt) < alertDate;
      if (shouldSendOverdueAlert) {
        const request = await db.getRequestById(followup.requestId);
        if (request?.status === "validee" && request.type === "demande_materiel_evenementiel") {
          const association = await db.getAssociationById(request.associationId);
          try {
            const result = await sendMaterialReturnFollowupEmail({
              followup,
              request,
              association,
              baseUrl,
              tone: "overdue",
            });
            if (result.sent) {
              await db.updateMaterialReturnFollowup(followup.id, {
                lastReminderSentAt: new Date(),
              });
            }
          } catch (error) {
            console.error("[MaterialReturnScheduler] Overdue reminder failed:", error);
          }
        }
      }

      await db.updateMaterialReturnFollowup(followup.id, {
        status: "en_cours",
      });
    }
  }

  const dueFollowups = await db.listPendingMaterialReturnFollowups(now);
  for (const followup of dueFollowups) {
    const request = await db.getRequestById(followup.requestId);
    if (!request || request.status !== "validee" || request.type !== "demande_materiel_evenementiel") {
      continue;
    }

    const association = await db.getAssociationById(request.associationId);

    try {
      const result = await sendMaterialReturnFollowupEmail({
        followup,
        request,
        association,
        baseUrl,
        tone: "scheduled",
      });

      if (result.sent) {
        await db.updateMaterialReturnFollowup(followup.id, {
          status: "en_attente",
          sentAt: new Date(),
          lastReminderSentAt: new Date(),
        });
      }
    } catch (error) {
      console.error("[MaterialReturnScheduler] Email send failed:", error);
    }
  }
}

export function serializeMaterialReturnFollowup(followup: Awaited<ReturnType<typeof db.getMaterialReturnFollowupByRequestId>>) {
  if (!followup) return null;
  let discrepancyCategories: string[] = [];
  try {
    const parsed = JSON.parse(followup.discrepancyCategories || "[]");
    if (Array.isArray(parsed)) {
      discrepancyCategories = parsed.map((entry) => String(entry || "")).filter(Boolean);
    }
  } catch {
    discrepancyCategories = [];
  }

  return {
    ...followup,
    recipientEmails: parseRecipientEmails(followup.recipientEmails),
    supervisionRecipientEmails: parseRecipientEmails(followup.supervisionRecipientEmails),
    serviceLabel: typeof followup.serviceLabel === "string" ? followup.serviceLabel : "",
    supervisionServiceLabel: typeof followup.supervisionServiceLabel === "string" ? followup.supervisionServiceLabel : "",
    status: typeof followup.status === "string" ? followup.status : "planifie",
    uploadLinkState: getMaterialReturnUploadLinkState({
      status: followup.status,
      uploadTokenExpiresAt: followup.uploadTokenExpiresAt,
    }),
    compliance: followup.compliance === "conforme" || followup.compliance === "non_conforme"
      ? followup.compliance
      : null,
    issueFlag: Boolean(followup.issueFlag),
    litigationStatus: followup.litigationStatus === "pending" ? "pending" : "none",
    geoFailureReason: typeof followup.geoFailureReason === "string" ? followup.geoFailureReason : null,
    discrepancyCategories,
    blockedItems: sanitizeMaterialEventQuantityMap(
      (() => {
        try {
          return followup.blockedItems ? JSON.parse(followup.blockedItems) : null;
        } catch {
          return null;
        }
      })()
    ),
  };
}
