import { prisma } from "@/lib/prisma";
import type { EmployeeAccessScope } from "@/services/employes.service";

const LESQUIN_ESTABLISHMENTS = new Set(["LESQUIN", "ENVIE LESQUIN"]);

function clean(value: string | null | undefined) {
  return (value ?? "").trim();
}

function normalize(value: string | null | undefined) {
  return clean(value)
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/\s+/g, " ")
    .toLocaleUpperCase("fr");
}

function valueAllowed(
  value: string | null | undefined,
  allowed: string[] | undefined,
) {
  if (allowed === undefined) return true;
  const normalized = normalize(value);
  return allowed.some((item) => normalize(item) === normalized);
}

type ContractScope = {
  Secteur: string | null;
  ETB: string | null;
  CAT: string | null;
  TCS: string | null;
};

function contractAllowed(contract: ContractScope, scope: EmployeeAccessScope) {
  return (
    valueAllowed(contract.Secteur, scope.sectors) &&
    valueAllowed(contract.ETB, scope.establishments) &&
    valueAllowed(contract.CAT, scope.categories) &&
    valueAllowed(contract.TCS, scope.contractTypes)
  );
}

function parseCalendarDate(value: string) {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
  if (!match) throw new Error("INVALID_DATE");
  const [, year, month, day] = match;
  const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 12));
  if (Number.isNaN(date.getTime())) throw new Error("INVALID_DATE");
  return date;
}

function startOfCalendarDay(value: string) {
  const date = parseCalendarDate(value);
  date.setUTCHours(0, 0, 0, 0);
  return date;
}

function endOfCalendarDay(value: string) {
  const date = parseCalendarDate(value);
  date.setUTCHours(23, 59, 59, 999);
  return date;
}

function monthRange(value: string) {
  const date = parseCalendarDate(value);
  return {
    start: new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0)),
    end: endOfCalendarDay(value),
  };
}

function isActiveAt(
  contract: { DSR: Date | null; DSP: Date | null },
  asOf: Date,
) {
  return (
    (contract.DSR == null || contract.DSR >= asOf) &&
    (contract.DSP == null || contract.DSP >= asOf)
  );
}

function decimal(value: { toString(): string } | null | undefined) {
  return value == null ? null : Number(value.toString());
}

const employeeSelection = {
  COS: true,
  TIT: true,
  NSA: true,
  PRE: true,
  ADR: true,
  COP: true,
  VIL: true,
  DAN: true,
  LNA: true,
  NSS: true,
  NAT: true,
  TypeCS: true,
  NCJ: true,
  EXJ: true,
  Cotorep: true,
  FinCotorep: true,
} as const;

const contractSelection = {
  id: true,
  Id_Salarie: true,
  TCS: true,
  MotifContrat: true,
  DAE: true,
  DSP: true,
  DSR: true,
  POS: true,
  QUA: true,
  CAT: true,
  NIV: true,
  ECH: true,
  COE: true,
  ETB: true,
  Secteur: true,
  CAN_Metiers: true,
  MSR: true,
  MEN: true,
  SMBE: true,
} as const;

async function activeEmployeesAt(
  date: string,
  scope: EmployeeAccessScope,
  lesquinOnly = true,
) {
  const asOf = startOfCalendarDay(date);
  const asOfEnd = endOfCalendarDay(date);
  const employees = await prisma.employes.findMany({
    orderBy: [{ NSA: "asc" }, { PRE: "asc" }],
    select: {
      ...employeeSelection,
      visites_medicales: {
        select: { id: true },
      },
      contrats: {
        where: { OR: [{ DAE: null }, { DAE: { lte: asOfEnd } }] },
        orderBy: [{ DAE: "desc" }, { id: "desc" }],
        take: 1,
        select: contractSelection,
      },
    },
  });

  return employees.flatMap((employee) => {
    const contract = employee.contrats[0];
    if (!contract || !isActiveAt(contract, asOf)) return [];
    if (lesquinOnly && !LESQUIN_ESTABLISHMENTS.has(normalize(contract.ETB))) return [];
    if (!contractAllowed(contract, scope)) return [];
    return [{ employee, contract }];
  });
}

function fullName(employee: { TIT: string | null; NSA: string | null; PRE: string | null }) {
  return [clean(employee.TIT), clean(employee.NSA), clean(employee.PRE)]
    .filter(Boolean)
    .join(" ");
}

export async function getMonthlyIndicators(
  date: string,
  scope: EmployeeAccessScope,
) {
  const active = await activeEmployeesAt(date, scope);
  const { start, end } = monthRange(date);
  const contractTypes = await prisma.types_contrat_salarie.findMany({
    select: { tcs: true, regroupement: true },
  });
  const groups = new Map(
    contractTypes.map((item) => [normalize(item.tcs), clean(item.regroupement) || clean(item.tcs) || "Autres"] as const),
  );

  const sectorMap = new Map<string, Map<string, number>>();
  const tradeMap = new Map<string, number>();
  for (const { contract } of active) {
    const sector = clean(contract.Secteur) || "Sans secteur";
    const grouping = groups.get(normalize(contract.TCS)) || clean(contract.TCS) || "Autres";
    const byContract = sectorMap.get(sector) ?? new Map<string, number>();
    byContract.set(grouping, (byContract.get(grouping) ?? 0) + 1);
    sectorMap.set(sector, byContract);
    const trade = clean(contract.CAN_Metiers) || "Non renseigné";
    tradeMap.set(trade, (tradeMap.get(trade) ?? 0) + 1);
  }

  const movements = await prisma.contrats.findMany({
    where: {
      OR: [
        { DAE: { gte: start, lte: end } },
        { DSR: { gte: start, lte: end } },
        { DSP: { gte: start, lte: end } },
      ],
    },
    select: contractSelection,
  });
  const scopedMovements = movements.filter(
    (contract) =>
      LESQUIN_ESTABLISHMENTS.has(normalize(contract.ETB)) &&
      contractAllowed(contract, scope),
  );
  const hireMap = new Map<string, number>();
  const exitMap = new Map<string, number>();
  for (const contract of scopedMovements) {
    const label = clean(contract.TCS) || "Non renseigné";
    if (contract.DAE && contract.DAE >= start && contract.DAE <= end) {
      hireMap.set(label, (hireMap.get(label) ?? 0) + 1);
    }
    const exitDate = contract.DSR ?? contract.DSP;
    if (exitDate && exitDate >= start && exitDate <= end) {
      exitMap.set(label, (exitMap.get(label) ?? 0) + 1);
    }
  }

  return {
    asOf: date,
    sectors: [...sectorMap.entries()]
      .sort(([left], [right]) => left.localeCompare(right, "fr"))
      .map(([sector, values]) => ({
        sector,
        rows: [...values.entries()]
          .sort(([left], [right]) => left.localeCompare(right, "fr"))
          .map(([group, count]) => ({ group, count })),
        total: [...values.values()].reduce((sum, count) => sum + count, 0),
      })),
    trades: [...tradeMap.entries()]
      .sort(([left], [right]) => left.localeCompare(right, "fr"))
      .map(([trade, count]) => ({ trade, count })),
    hires: [...hireMap.entries()].map(([contractType, count]) => ({ contractType, count })),
    exits: [...exitMap.entries()].map(([contractType, count]) => ({ contractType, count })),
    disabledEmployees: active
      .filter(({ employee }) => employee.Cotorep)
      .map(({ employee }) => ({ cos: employee.COS, name: fullName(employee) })),
    total: active.length,
  };
}

export async function getPayrollElements(
  date: string,
  scope: EmployeeAccessScope,
) {
  const active = await activeEmployeesAt(date, scope);
  const { start, end } = monthRange(date);
  const absences = await prisma.absences.findMany({
    where: {
      datedu: { lte: end },
      OR: [{ dateau: null }, { dateau: { gte: start } }],
    },
    orderBy: [{ datedu: "asc" }, { id: "asc" }],
  });
  const byEmployee = new Map<number, typeof absences>();
  for (const absence of absences) {
    if (absence.num_salarie == null) continue;
    byEmployee.set(absence.num_salarie, [...(byEmployee.get(absence.num_salarie) ?? []), absence]);
  }

  return {
    asOf: date,
    rows: active.map(({ employee, contract }) => {
      const employeeAbsences = employee.COS == null ? [] : byEmployee.get(employee.COS) ?? [];
      return {
        cos: employee.COS,
        lastName: clean(employee.NSA),
        firstName: clean(employee.PRE),
        hireDate: contract.DAE,
        contractType: clean(contract.TCS),
        qualification: clean(contract.QUA) || clean(contract.POS),
        grossSalary: decimal(contract.SMBE),
        monthlyHours: decimal(contract.MEN),
        absences: employeeAbsences.map((absence) => ({
          from: absence.datedu,
          to: absence.dateau,
          reason: clean(absence.typeabsence),
          hours: decimal(absence.nbheures),
        })),
      };
    }),
  };
}

export async function getEmployeesWithoutMedicalVisit(
  date: string,
  scope: EmployeeAccessScope,
) {
  const active = await activeEmployeesAt(date, scope);
  return {
    asOf: date,
    rows: active
      .filter(({ employee }) => employee.visites_medicales.length === 0)
      .map(({ employee, contract }) => ({
        cos: employee.COS,
        title: clean(employee.TIT),
        lastName: clean(employee.NSA),
        firstName: clean(employee.PRE),
        job: clean(contract.QUA) || clean(contract.POS),
        contractType: clean(contract.TCS),
        hireDate: contract.DAE,
      })),
  };
}

export async function getEntryExitRegister(
  from: string,
  to: string,
  scope: EmployeeAccessScope,
) {
  const start = startOfCalendarDay(from);
  const end = endOfCalendarDay(to);
  if (start > end) throw new Error("INVALID_RANGE");
  const contracts = await prisma.contrats.findMany({
    where: {
      OR: [
        { DAE: { gte: start, lte: end } },
        { DSR: { gte: start, lte: end } },
        { DSP: { gte: start, lte: end } },
      ],
    },
    orderBy: [{ DAE: "asc" }, { id: "asc" }],
    select: { ...contractSelection, employes: { select: employeeSelection } },
  });

  const scoped = contracts.filter(
    (contract) =>
      contract.employes != null &&
      LESQUIN_ESTABLISHMENTS.has(normalize(contract.ETB)) &&
      contractAllowed(contract, scope),
  );
  const row = (contract: (typeof scoped)[number]) => ({
    contractId: contract.id,
    cos: contract.employes?.COS,
    name: [clean(contract.employes?.NSA), clean(contract.employes?.PRE)].filter(Boolean).join(" "),
    socialSecurityNumber: clean(contract.employes?.NSS),
    nationality: clean(contract.employes?.NAT),
    birthDate: contract.employes?.DAN,
    job: clean(contract.QUA) || clean(contract.POS),
    qualification: [clean(contract.CAT), clean(contract.NIV), clean(contract.ECH)].filter(Boolean).join(" "),
    hireDate: contract.DAE,
    exitDate: contract.DSR ?? contract.DSP,
    contractType: clean(contract.TCS),
    exitReason: clean(contract.MSR),
    residencePermitType: clean(contract.employes?.TypeCS),
    residencePermitNumber: clean(contract.employes?.NCJ),
  });

  return {
    from,
    to,
    entries: scoped.filter((contract) => contract.DAE && contract.DAE >= start && contract.DAE <= end).map(row),
    exits: scoped.filter((contract) => {
      const exit = contract.DSR ?? contract.DSP;
      return exit != null && exit >= start && exit <= end;
    }).map(row),
  };
}

export async function getForeignEmployeesTenderDocument(
  date: string,
  scope: EmployeeAccessScope,
) {
  const active = await activeEmployeesAt(date, scope);
  const employees = active
    .filter(({ employee }) => clean(employee.TypeCS) || clean(employee.NCJ))
    .map(({ employee, contract }) => ({
      cos: employee.COS,
      lastName: clean(employee.NSA),
      firstName: clean(employee.PRE),
      hireDate: contract.DAE,
      nationality: clean(employee.NAT),
      residencePermitType: clean(employee.TypeCS),
      residencePermitNumber: clean(employee.NCJ),
    }));
  return { asOf: date, employees };
}

export async function getNightTeamEmployeeOptions(
  date: string,
  scope: EmployeeAccessScope,
) {
  const active = await activeEmployeesAt(date, scope);
  return {
    asOf: date,
    employees: active.map(({ employee }) => ({
      cos: employee.COS,
      title: clean(employee.TIT),
      lastName: clean(employee.NSA),
      firstName: clean(employee.PRE),
    })),
  };
}

export type NightTeamRequest = {
  startDate: string;
  endDate: string;
  returnDate: string;
  signedAt: string;
  employees: Array<{ cos: number; role: "OUVRIER" | "CHEF_EQUIPE" }>;
};

export async function createNightTeamDocuments(
  input: NightTeamRequest,
  scope: EmployeeAccessScope,
) {
  const start = parseCalendarDate(input.startDate);
  const end = parseCalendarDate(input.endDate);
  const returnDate = parseCalendarDate(input.returnDate);
  const signedAt = parseCalendarDate(input.signedAt);
  if (start > end || returnDate <= end) throw new Error("INVALID_RANGE");
  const uniqueEmployees = [...new Map(input.employees.map((item) => [item.cos, item] as const)).values()];
  if (uniqueEmployees.length === 0) throw new Error("EMPLOYEES_REQUIRED");

  const active = await activeEmployeesAt(input.signedAt, scope);
  const byCos = new Map(active.map((item) => [item.employee.COS, item] as const));
  const selected = uniqueEmployees.map((item) => {
    const found = byCos.get(item.cos);
    if (!found) throw new Error("EMPLOYEE_NOT_FOUND");
    return { ...found, role: item.role };
  });

  const historyRows = selected.map(({ employee, role }) => ({
      cos: employee.COS == null ? null : String(employee.COS),
      prenom_nom: [clean(employee.PRE), clean(employee.NSA)].filter(Boolean).join(" "),
      role: role === "CHEF_EQUIPE" ? "Chef d'équipe" : "Ouvrier",
      equipenuit_debut: start,
      equipenuit_fin: end,
      equipenuit_reprise: returnDate,
      equipenuit_faitle: signedAt,
  }));
  const existingHistory = await prisma.equipenuit.findMany({
    where: {
      cos: { in: historyRows.map((row) => row.cos).filter((cos): cos is string => cos != null) },
      equipenuit_debut: start,
      equipenuit_fin: end,
      equipenuit_reprise: returnDate,
      equipenuit_faitle: signedAt,
    },
    select: { cos: true, role: true },
  });
  const existingKeys = new Set(existingHistory.map((row) => `${row.cos}|${row.role}`));
  const historyToCreate = historyRows.filter((row) => !existingKeys.has(`${row.cos}|${row.role}`));
  if (historyToCreate.length > 0) {
    await prisma.equipenuit.createMany({ data: historyToCreate });
  }

  const establishment = await prisma.etablissement.findFirst({
    where: { OR: [{ etb: "LESQUIN" }, { etb: "ENVIE LESQUIN" }] },
    select: { entreprise: true, directeur: true, adressesiege: true, adresseetb: true },
  });
  return {
    ...input,
    company: clean(establishment?.entreprise) || "S.A.S. ENVIE 2E NORD",
    director: clean(establishment?.directeur) || "Le Directeur",
    companyAddress:
      clean(establishment?.adresseetb) ||
      clean(establishment?.adressesiege) ||
      "2 boulevard Thomson – CS 60500 – 59815 LESQUIN CEDEX",
    documents: selected.map(({ employee, role }) => ({
      cos: employee.COS,
      role,
      title: clean(employee.TIT),
      lastName: clean(employee.NSA),
      firstName: clean(employee.PRE),
      birthDate: employee.DAN,
      birthPlace: clean(employee.LNA),
      nationality: clean(employee.NAT),
      socialSecurityNumber: clean(employee.NSS),
      address: clean(employee.ADR),
      postalCode: clean(employee.COP),
      city: clean(employee.VIL),
    })),
  };
}
