import { http, HttpResponse } from "msw";
import { z } from "zod";
import { invoiceListFiltersSchema } from "@rubis/shared";
import type { Client, InvoiceStatus, Plan, PlanStep } from "@rubis/shared";

import { mockDb, type DraftFields, type StoredInvoice } from "../db";
import { userIdFromAuthHeader } from "./auth";

const apiBase = "*/api/v1";

function unauthenticated() {
  return HttpResponse.json(
    { errors: [{ code: "unauthenticated", message: "Non authentifié" }] },
    { status: 401 },
  );
}

function notFound() {
  return HttpResponse.json(
    { errors: [{ code: "not_found", message: "Facture introuvable" }] },
    { status: 404 },
  );
}

function authedOrgId(authHeader: string | null): string | undefined {
  const userId = userIdFromAuthHeader(authHeader);
  if (!userId) return undefined;
  return mockDb.findUserById(userId)?.organizationId;
}

/* ----------------------------------------------------------------------------
 * OCR mock — extraction "intelligente" depuis un nom de fichier.
 * Pour faire vivre la démo, on génère des champs plausibles et on injecte
 * volontairement quelques confidences basses pour signaler "champ douteux".
 * -------------------------------------------------------------------------- */

const SAMPLE_CLIENT_NAMES = [
  "Boulangerie Martin SARL",
  "Atelier Durand",
  "Cabinet Rousseau",
  "Garage Lemoine",
  "Studio Lefèvre",
  "Pharmacie Bertrand",
  "Imprimerie Moreau",
];

function rand<T>(arr: readonly T[]): T {
  return arr[Math.floor(Math.random() * arr.length)]!;
}

function randomAmountCents(): number {
  // entre 80 € et 8 000 €, multiple de 50 cents pour rester crédible
  return Math.floor((Math.random() * 7920 + 80) * 100 / 50) * 50;
}

function randomNumeroFromFilename(filename: string): string {
  const match = filename.match(/(\d{2,5})/u);
  const yr = new Date().getFullYear();
  const seq = match?.[1] ?? Math.floor(Math.random() * 9000 + 1000).toString();
  return `F-${yr}-${seq.padStart(4, "0")}`;
}

function isoDaysFromNow(days: number): string {
  const d = new Date();
  d.setDate(d.getDate() + days);
  d.setHours(9, 0, 0, 0);
  return d.toISOString();
}

function fakeOcrExtract(
  orgId: string,
  filename: string,
  defaultPlanId: string | null,
): {
  extracted: DraftFields;
  confidence: Partial<Record<keyof DraftFields, number>>;
} {
  const clientName = rand(SAMPLE_CLIENT_NAMES);
  // 30 % de chance d'avoir un email douteux (low confidence)
  const emailLowConf = Math.random() < 0.3;
  const slug = clientName
    .toLowerCase()
    .replace(/sarl|sa|sas/giu, "")
    .replace(/[^a-z]+/giu, "-")
    .replace(/^-+|-+$/gu, "");

  // Si l'OCR a "extrait" un nom qui matche un client existant, on lie tout
  // de suite au clientId : l'utilisateur voit son client déjà sélectionné
  // dans le combobox et n'a rien à faire.
  const matchedClient = mockDb
    .listClientsForOrg(orgId)
    .find((c) => c.name.toLowerCase() === clientName.toLowerCase());

  return {
    extracted: {
      clientId: matchedClient?.id ?? null,
      clientName: matchedClient?.name ?? clientName,
      clientEmail: matchedClient?.email ?? (emailLowConf ? null : `compta@${slug}.fr`),
      numero: randomNumeroFromFilename(filename),
      amountTtcCents: randomAmountCents(),
      issueDate: isoDaysFromNow(-15 - Math.floor(Math.random() * 10)),
      dueDate: isoDaysFromNow(15 + Math.floor(Math.random() * 20)),
      planId: defaultPlanId,
    },
    confidence: {
      clientName: matchedClient ? 1 : 0.95,
      clientEmail: emailLowConf ? 0.42 : 0.88,
      numero: 0.97,
      amountTtcCents: 0.93,
      issueDate: 0.9,
      dueDate: emailLowConf ? 0.65 : 0.92,
      planId: 1,
    },
  };
}

const uploadSchema = z.object({
  filenames: z.array(z.string().min(1)).min(1).max(20),
});

async function filenamesFromUploadRequest(request: Request): Promise<string[]> {
  const contentType = request.headers.get("content-type") ?? "";
  if (contentType.startsWith("multipart/")) {
    return request
      .formData()
      .then((formData) =>
        formData
          .getAll("files")
          .filter((file): file is File => file instanceof File)
          .map((file) => file.name),
      );
  }

  const json = await request.json();
  const parsed = uploadSchema.safeParse(json);
  if (!parsed.success) {
    throw parsed.error;
  }
  return parsed.data.filenames;
}

const draftFieldsSchema = z.object({
  clientId: z.string().nullable(),
  clientName: z.string().min(1).max(120),
  clientEmail: z.string().email().nullable(),
  numero: z.string().min(1).max(50),
  amountTtcCents: z.number().int().positive(),
  issueDate: z.string().datetime(),
  dueDate: z.string().datetime(),
  planId: z.string().nullable(),
});

const createInvoiceManualSchema = z.object({
  clientId: z.string().optional(),
  clientName: z.string().min(2).max(120),
  clientEmail: z.string().email().nullable().optional(),
  numero: z.string().min(1).max(50),
  amountTtcCents: z.number().int().positive(),
  issueDate: z.string().datetime(),
  dueDate: z.string().datetime(),
  planId: z.string().nullable().optional(),
});

/**
 * Construit la timeline d'une facture en composant les étapes du plan
 * avec l'état courant. Très simplifié pour V1 :
 *  - étapes dont sendDay <= aujourd'hui : "past" (envoyées)
 *  - étape actuelle (la prochaine future) : "current"
 *  - étapes futures : "future"
 *
 * On ajoute un événement initial "facture émise".
 */
function buildTimeline(
  invoice: StoredInvoice,
  plan: Plan | null,
): Array<{
  id: string;
  state: "past" | "current" | "future";
  when: string;
  what: string;
  detail?: string;
}> {
  const events: ReturnType<typeof buildTimeline> = [
    {
      id: `${invoice.id}__issued`,
      state: "past",
      when: `${formatShortDate(invoice.issueDate)} · facture émise`,
      what: "Importée · OCR validée",
    },
  ];

  if (plan && invoice.status !== "paid" && invoice.status !== "cancelled") {
    const dueMs = new Date(invoice.dueDate).getTime();
    const nowMs = Date.now();
    let currentSet = false;

    for (const step of plan.steps) {
      const sendMs = dueMs + step.offsetDays * 24 * 60 * 60 * 1000;
      const sendDate = new Date(sendMs);
      const labelStep = `J${step.offsetDays >= 0 ? "+" : ""}${step.offsetDays} — Étape ${step.order + 1}`;

      let state: "past" | "current" | "future";
      if (sendMs < nowMs) {
        state = "past";
      } else if (!currentSet) {
        state = "current";
        currentSet = true;
      } else {
        state = "future";
      }

      events.push({
        id: `${invoice.id}__step_${step.order}`,
        state,
        when: `${formatShortDate(sendDate.toISOString())} · ${labelStep}`,
        what:
          state === "past"
            ? `Email envoyé · "${step.subject.replace("{{numero}}", invoice.numero)}"`
            : state === "current"
            ? `Email programmé · "${step.subject.replace("{{numero}}", invoice.numero)}"`
            : `Email programmé · "${step.subject.replace("{{numero}}", invoice.numero)}"`,
        detail: state === "past" ? "Ouvert 1×" : undefined,
      });
    }
  }

  if (invoice.status === "paid") {
    events.push({
      id: `${invoice.id}__paid`,
      state: "past",
      when: `${formatShortDate(invoice.updatedAt)} · facture encaissée`,
      what: "Marquée encaissée — relances stoppées",
    });
  }

  return events;
}

function formatShortDate(iso: string): string {
  const d = new Date(iso);
  return `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1)
    .toString()
    .padStart(2, "0")}/${d.getFullYear()}`;
}

export const invoiceHandlers = [
  // GET /api/v1/invoices?status=&q=&clientId=&page=
  http.get(`${apiBase}/invoices`, ({ request }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const url = new URL(request.url);
    const params = Object.fromEntries(url.searchParams.entries());
    const parsed = invoiceListFiltersSchema.safeParse({
      ...params,
      page: params.page ? Number(params.page) : undefined,
    });

    let invoices = mockDb.listInvoicesForOrg(orgId);
    const filters = parsed.success ? parsed.data : { page: 1 };

    if (filters.status && filters.status !== "all") {
      invoices = invoices.filter((i) => i.status === (filters.status as InvoiceStatus));
    }
    if (filters.clientId) {
      invoices = invoices.filter((i) => i.clientId === filters.clientId);
    }
    if (filters.q) {
      const q = filters.q.toLowerCase();
      invoices = invoices.filter(
        (i) =>
          i.numero.toLowerCase().includes(q) ||
          i.clientName.toLowerCase().includes(q),
      );
    }

    // Tri : à relancer / en relance d'abord (les plus actionnables), puis par échéance
    const STATUS_PRIO: Record<InvoiceStatus, number> = {
      awaiting_user_confirmation: 0,
      in_relance: 1,
      pending: 2,
      litigation: 3,
      paid: 4,
      cancelled: 5,
    };
    invoices = [...invoices].sort((a, b) => {
      const dp = STATUS_PRIO[a.status] - STATUS_PRIO[b.status];
      if (dp !== 0) return dp;
      return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
    });

    return HttpResponse.json({
      data: invoices,
      meta: {
        total: invoices.length,
        page: filters.page ?? 1,
      },
    });
  }),

  // GET /api/v1/invoices/counts — compteurs par statut pour les chips
  http.get(`${apiBase}/invoices/counts`, ({ request }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const invoices = mockDb.listInvoicesForOrg(orgId);
    const counts = {
      all: invoices.length,
      pending: invoices.filter((i) => i.status === "pending").length,
      in_relance: invoices.filter((i) => i.status === "in_relance").length,
      awaiting_user_confirmation: invoices.filter(
        (i) => i.status === "awaiting_user_confirmation",
      ).length,
      paid: invoices.filter((i) => i.status === "paid").length,
      litigation: invoices.filter((i) => i.status === "litigation").length,
      cancelled: invoices.filter((i) => i.status === "cancelled").length,
    };
    return HttpResponse.json({ data: counts });
  }),

  // GET /api/v1/invoices/:id — détail enrichi (client + plan + timeline)
  http.get(`${apiBase}/invoices/:id`, ({ request, params }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const id = params.id as string;
    const invoice = mockDb.findInvoiceById(orgId, id);
    if (!invoice) return notFound();

    const client = mockDb.findClientById(orgId, invoice.clientId) as Client;
    const plan = invoice.planId ? mockDb.findPlanById(orgId, invoice.planId) ?? null : null;
    const timeline = buildTimeline(invoice, plan);

    return HttpResponse.json({
      data: {
        ...invoice,
        client,
        plan,
        timeline,
      },
    });
  }),

  // GET /api/v1/clients — autocomplete dans la saisie manuelle
  http.get(`${apiBase}/clients`, ({ request }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();
    return HttpResponse.json({ data: mockDb.listClientsForOrg(orgId) });
  }),

  // POST /api/v1/invoices — saisie manuelle (cf. wireframe 2.3)
  http.post(`${apiBase}/invoices`, async ({ request }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const json = await request.json();
    const parsed = createInvoiceManualSchema.safeParse(json);
    if (!parsed.success) {
      return HttpResponse.json(
        {
          errors: parsed.error.issues.map((i) => ({
            code: "validation_failed",
            message: i.message,
            field: i.path.join("."),
          })),
        },
        { status: 422 },
      );
    }
    const fields = parsed.data;

    // Résolution client : si clientId fourni → utilise. Sinon match par nom
    // (case-insensitive). Sinon crée un nouveau client à la volée — mais
    // dans ce cas l'email est OBLIGATOIRE car sans email Rubis ne peut
    // pas envoyer les relances (cf. décision produit).
    let clientId = fields.clientId;
    let clientName = fields.clientName;
    if (!clientId) {
      const existing = mockDb
        .listClientsForOrg(orgId)
        .find(
          (c) => c.name.toLowerCase() === fields.clientName.toLowerCase(),
        );
      if (existing) {
        clientId = existing.id;
        clientName = existing.name;
      } else {
        if (!fields.clientEmail || fields.clientEmail.length === 0) {
          return HttpResponse.json(
            {
              errors: [
                {
                  code: "client_email_required",
                  message:
                    "Email du client requis pour créer une nouvelle fiche — Rubis en a besoin pour envoyer les relances.",
                  field: "clientEmail",
                },
              ],
            },
            { status: 422 },
          );
        }
        const created = mockDb.createClient(orgId, {
          name: fields.clientName,
          email: fields.clientEmail,
          contactFirstName: null,
          contactLastName: null,
          phone: null,
          address: null,
          siret: null,
          notes: null,
        });
        clientId = created.id;
        clientName = created.name;
      }
    } else {
      const c = mockDb.findClientById(orgId, clientId);
      if (c) clientName = c.name;
    }

    const plan = fields.planId ? mockDb.findPlanById(orgId, fields.planId) : null;

    const invoice = mockDb.createInvoice(orgId, {
      clientId: clientId!,
      clientName,
      numero: fields.numero,
      amountTtcCents: fields.amountTtcCents,
      issueDate: fields.issueDate,
      dueDate: fields.dueDate,
      status: "pending",
      planId: plan?.id ?? null,
      planName: plan?.name ?? null,
      pdfStorageKey: null,
      notes: null,
      rubisEarned: 1, // bonus saisie
    });

    return HttpResponse.json({ data: invoice }, { status: 201 });
  }),

  // POST /api/v1/invoices/upload — démarre un batch OCR
  http.post(`${apiBase}/invoices/upload`, async ({ request }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    let filenames: string[];
    try {
      filenames = await filenamesFromUploadRequest(request);
      uploadSchema.parse({ filenames });
    } catch (err) {
      return HttpResponse.json(
        {
          errors: (err instanceof z.ZodError ? err.issues : []).map((i) => ({
            code: "validation_failed",
            message: i.message,
          })),
        },
        { status: 422 },
      );
    }

    // Plan par défaut = le premier "isDefault" disponible
    const plans = mockDb.listPlansForOrg(orgId);
    const defaultPlanId = plans.find((p) => p.isDefault)?.id ?? null;

    const drafts = filenames.map((filename) => {
      const { extracted, confidence } = fakeOcrExtract(orgId, filename, defaultPlanId);
      return { filename, extracted, confidence };
    });

    const batch = mockDb.createImportBatch(orgId, drafts);

    // Petit délai pour simuler le temps de l'OCR (perceptible UX-wise)
    await new Promise((r) => setTimeout(r, 800));

    return HttpResponse.json({ data: batch }, { status: 201 });
  }),

  // GET /api/v1/invoices/import-batch/:id
  http.get(`${apiBase}/invoices/import-batch/:id`, ({ request, params }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const batch = mockDb.findImportBatch(orgId, params.id as string);
    if (!batch) return notFound();
    return HttpResponse.json({ data: batch });
  }),

  // POST /api/v1/invoices/import-batch/:id/drafts/:draftId/validate
  http.post(
    `${apiBase}/invoices/import-batch/:id/drafts/:draftId/validate`,
    async ({ request, params }) => {
      const orgId = authedOrgId(request.headers.get("authorization"));
      if (!orgId) return unauthenticated();

      const json = await request.json();
      const parsed = draftFieldsSchema.safeParse(json);
      if (!parsed.success) {
        return HttpResponse.json(
          {
            errors: parsed.error.issues.map((i) => ({
              code: "validation_failed",
              message: i.message,
              field: i.path.join("."),
            })),
          },
          { status: 422 },
        );
      }

      const fields = parsed.data;

      // Résolution client (priorité descendante) :
      //  1. clientId explicite envoyé depuis le combobox → utilise direct
      //  2. match par nom (case-insensitive) sur les clients existants
      //  3. création à la volée si rien ne matche — email obligatoire
      //     puisqu'on n'accepte pas de fiche client sans canal de relance.
      let clientId: string;
      let clientName: string;
      const requireEmailForCreation = () => {
        if (!fields.clientEmail || fields.clientEmail.length === 0) {
          return HttpResponse.json(
            {
              errors: [
                {
                  code: "client_email_required",
                  message:
                    "Email du client requis — Rubis en a besoin pour envoyer les relances.",
                  field: "clientEmail",
                },
              ],
            },
            { status: 422 },
          );
        }
        return null;
      };

      if (fields.clientId) {
        const c = mockDb.findClientById(orgId, fields.clientId);
        if (c) {
          clientId = c.id;
          clientName = c.name;
        } else {
          // clientId fourni mais introuvable — fallback création
          const err = requireEmailForCreation();
          if (err) return err;
          const created = mockDb.createClient(orgId, {
            name: fields.clientName,
            email: fields.clientEmail!,
            contactFirstName: null,
            contactLastName: null,
            phone: null,
            address: null,
            siret: null,
            notes: null,
          });
          clientId = created.id;
          clientName = created.name;
        }
      } else {
        const matched = mockDb
          .listClientsForOrg(orgId)
          .find(
            (c) => c.name.toLowerCase() === fields.clientName.toLowerCase(),
          );
        if (matched) {
          clientId = matched.id;
          clientName = matched.name;
        } else {
          const err = requireEmailForCreation();
          if (err) return err;
          const created = mockDb.createClient(orgId, {
            name: fields.clientName,
            email: fields.clientEmail!,
            contactFirstName: null,
            contactLastName: null,
            phone: null,
            address: null,
            siret: null,
            notes: null,
          });
          clientId = created.id;
          clientName = created.name;
        }
      }

      const plan = fields.planId
        ? mockDb.findPlanById(orgId, fields.planId)
        : null;

      const invoice = mockDb.createInvoice(orgId, {
        clientId,
        clientName,
        numero: fields.numero,
        amountTtcCents: fields.amountTtcCents,
        issueDate: fields.issueDate,
        dueDate: fields.dueDate,
        status: "pending",
        planId: plan?.id ?? null,
        planName: plan?.name ?? null,
        pdfStorageKey: null,
        notes: null,
        rubisEarned: 1, // bonus rubis : 1 import = 1 rubis
      });

      mockDb.updateImportDraft(orgId, params.id as string, params.draftId as string, {
        status: "validated",
        edited: fields,
        invoiceId: invoice.id,
      });

      return HttpResponse.json({ data: invoice }, { status: 201 });
    },
  ),

  // POST /api/v1/invoices/import-batch/:id/drafts/:draftId/skip
  http.post(
    `${apiBase}/invoices/import-batch/:id/drafts/:draftId/skip`,
    ({ request, params }) => {
      const orgId = authedOrgId(request.headers.get("authorization"));
      if (!orgId) return unauthenticated();

      const draft = mockDb.updateImportDraft(
        orgId,
        params.id as string,
        params.draftId as string,
        { status: "skipped" },
      );
      if (!draft) return notFound();
      return HttpResponse.json({ data: draft });
    },
  ),

  // DELETE /api/v1/invoices/import-batch/:id — annule le batch entier
  http.delete(`${apiBase}/invoices/import-batch/:id`, ({ request, params }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const ok = mockDb.deleteImportBatch(orgId, params.id as string);
    if (!ok) return notFound();
    return new HttpResponse(null, { status: 204 });
  }),

  // POST /api/v1/invoices/:id/mark-paid
  http.post(`${apiBase}/invoices/:id/mark-paid`, ({ request, params }) => {
    const orgId = authedOrgId(request.headers.get("authorization"));
    if (!orgId) return unauthenticated();

    const id = params.id as string;
    const invoice = mockDb.findInvoiceById(orgId, id);
    if (!invoice) return notFound();

    const updated = mockDb.updateInvoice(orgId, id, {
      status: "paid",
      // +1 rubis bonus quand on encaisse — gamification
      rubisEarned: invoice.rubisEarned + 1,
    });
    return HttpResponse.json({ data: updated });
  }),
];

// Suppress unused warnings for the imported types we re-export indirectly.
export type { PlanStep };
