import { FastifyInstance } from "fastify";
import multipart from "@fastify/multipart";
import { received_invoices_status } from "@prisma/client";
import { z } from "zod";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response";
import { contentDisposition } from "../../utils/content-disposition";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import {
  CreateReceivedInvoiceSchema,
  UpdateReceivedInvoiceSchema,
} from "../../schemas/received-invoices.schema";
import { nasInvoicesManager } from "../../services/nas-financials-manager";
import { toCzk } from "../../services/exchange-rates";
import { utcMidnightOfLocalDay } from "../../utils/date";
import path from "path";

const VALID_STATUSES = ["unpaid", "paid"] as const;

/** Round a monetary value to 2 decimal places to avoid floating-point drift. */
function roundMoney(n: number): number {
  return Math.round(n * 100) / 100;
}

/**
 * VAT contained WITHIN a gross (VAT-inclusive) amount. `amount` is the total to
 * pay including tax, so the tax portion is gross * rate / (100 + rate) — e.g.
 * 22542.91 @ 21% → 3912.41 (base 18630.50). Returns 0 when there is no VAT.
 */
export function vatFromGross(gross: number, rate: number): number {
  return rate > 0 ? roundMoney((gross * rate) / (100 + rate)) : 0;
}
const ALLOWED_SORT_FIELDS = [
  "id",
  "supplier_name",
  "amount",
  "issue_date",
  "due_date",
  "status",
  "created_at",
];

interface ReceivedInvoiceFilterParams {
  year?: number;
  month?: number;
  status?: string;
  supplier_name?: string;
  search?: string;
}

/**
 * Shared `where` builder for the received-invoices list and the per-currency
 * list-totals aggregation, so both stay in sync. The list filters by
 * year/month/status/supplier_name/search.
 */
export function buildReceivedWhere(
  params: ReceivedInvoiceFilterParams,
): Record<string, unknown> {
  const { year, month, status, supplier_name, search } = params;
  const where: Record<string, unknown> = {};
  if (year) where.year = year;
  if (month) where.month = month;
  if (status) where.status = status;
  if (supplier_name) where.supplier_name = { contains: supplier_name };
  if (search) {
    where.OR = [
      { supplier_name: { contains: search } },
      { invoice_number: { contains: search } },
      { description: { contains: search } },
    ];
  }
  return where;
}

/**
 * Sum received-invoice GROSS `amount` per currency over the WHOLE filtered set
 * (not one page), reusing `buildReceivedWhere` so the totals track the list.
 * `amount` is VAT-inclusive (GROSS), so it is summed directly. Returns one
 * entry per currency, rounded to 2dp, zero/empty totals dropped. Exported so
 * the route and the totals test share one implementation.
 */
export async function getReceivedInvoiceListTotals(
  params: ReceivedInvoiceFilterParams,
): Promise<{ totals: Array<{ amount: number; currency: string }> }> {
  const where = buildReceivedWhere(params);

  const invoices = await prisma.received_invoices.findMany({
    where,
    select: { amount: true, currency: true },
  });

  const byCurrency: Record<string, number> = {};
  for (const inv of invoices) {
    const cur = inv.currency || "CZK";
    byCurrency[cur] = (byCurrency[cur] || 0) + (Number(inv.amount) || 0);
  }

  const totals = Object.entries(byCurrency)
    .map(([currency, amount]) => ({
      currency,
      amount: Math.round(amount * 100) / 100,
    }))
    .filter((t) => t.amount > 0);

  return { totals };
}

export default async function receivedInvoicesRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  await fastify.register(multipart, { limits: { fileSize: 50 * 1024 * 1024 } });

  fastify.get(
    "/",
    { preHandler: requirePermission("invoices.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip, order } = parsePagination(query);

      const where = buildReceivedWhere({
        year: query.year ? Number(query.year) : undefined,
        month: query.month ? Number(query.month) : undefined,
        status: query.status ? String(query.status) : undefined,
        supplier_name: query.supplier_name
          ? String(query.supplier_name)
          : undefined,
        search: query.search ? String(query.search) : undefined,
      });

      // Sort field whitelisting
      const sortField =
        query.sort && ALLOWED_SORT_FIELDS.includes(String(query.sort))
          ? String(query.sort)
          : "id";

      const [invoices, total] = await Promise.all([
        prisma.received_invoices.findMany({
          where,
          skip,
          take: limit,
          orderBy: { [sortField]: order },
        }),
        prisma.received_invoices.count({ where }),
      ]);

      return paginated(
        reply,
        invoices,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /api/admin/received-invoices/stats
  fastify.get(
    "/stats",
    { preHandler: requirePermission("invoices.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const now = new Date();
      const year = Number(query.year) || now.getFullYear();
      const month = Number(query.month) || now.getMonth() + 1;

      const where: Record<string, unknown> = { year, month };
      const monthInvoices = await prisma.received_invoices.findMany({ where });

      // Aggregate by currency → CurrencyAmount[] format
      const aggregateByCurrency = (
        invs: { currency: string; [key: string]: unknown }[],
        field: "amount" | "vat_amount",
      ) => {
        const map: Record<string, number> = {};
        for (const inv of invs) {
          const cur = inv.currency || "CZK";
          map[cur] = (map[cur] || 0) + (Number(inv[field]) || 0);
        }
        return Object.entries(map)
          .filter(([, v]) => v > 0)
          .map(([currency, amount]) => ({
            amount: Math.round(amount * 100) / 100,
            currency,
          }));
      };

      const sumCzk = async (
        invs: { currency: string; [key: string]: unknown }[],
        field: "amount" | "vat_amount",
      ) => {
        let total = 0;
        for (const inv of invs) {
          const amount = Number(inv[field]) || 0;
          // `toCzk` throws on an unknown/unsupported currency. A single bad row
          // must not 500 the whole stats endpoint — log and skip it instead.
          try {
            total += await toCzk(amount, inv.currency);
          } catch (e) {
            request.log.warn(
              e,
              `Přeskočen převod na CZK pro měnu "${inv.currency}" ve statistikách přijatých faktur`,
            );
          }
        }
        return Math.round(total * 100) / 100;
      };

      // All-time unpaid invoices (no soft-delete column, so just filter by status)
      const unpaidCount = await prisma.received_invoices.count({
        where: { status: { not: "paid" } },
      });
      const allUnpaid = await prisma.received_invoices.findMany({
        where: { status: { not: "paid" } },
        select: { amount: true, currency: true },
      });

      return success(reply, {
        total_month: aggregateByCurrency(monthInvoices, "amount"),
        total_month_czk: await sumCzk(monthInvoices, "amount"),
        vat_month: aggregateByCurrency(monthInvoices, "vat_amount"),
        vat_month_czk: await sumCzk(monthInvoices, "vat_amount"),
        unpaid: aggregateByCurrency(allUnpaid, "amount"),
        unpaid_czk: await sumCzk(allUnpaid, "amount"),
        unpaid_count: unpaidCount,
        month_count: monthInvoices.length,
      });
    },
  );

  // GET /api/admin/received-invoices/list-totals — per-currency total summed
  // over the WHOLE filtered set (not one page), matching the received-invoice
  // list's filters (year/month/status/supplier_name/search). `amount` is GROSS
  // (VAT-inclusive), so it is summed directly per currency. Distinct path from
  // "/stats" (the monthly KPI cards) to avoid colliding with that endpoint.
  // Registered BEFORE "/:id" so the literal path isn't captured as an id.
  fastify.get(
    "/list-totals",
    { preHandler: requirePermission("invoices.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const result = await getReceivedInvoiceListTotals({
        year: query.year ? Number(query.year) : undefined,
        month: query.month ? Number(query.month) : undefined,
        status: query.status ? String(query.status) : undefined,
        supplier_name: query.supplier_name
          ? String(query.supplier_name)
          : undefined,
        search: query.search ? String(query.search) : undefined,
      });
      return success(reply, { totals: result.totals });
    },
  );

  // GET /api/admin/received-invoices/suppliers — distinct supplier names for autocomplete
  fastify.get(
    "/suppliers",
    { preHandler: requirePermission("invoices.view") },
    async (_request, reply) => {
      const results = await prisma.received_invoices.findMany({
        select: { supplier_name: true },
        distinct: ["supplier_name"],
        orderBy: { supplier_name: "asc" },
      });
      return success(
        reply,
        results.map((r) => r.supplier_name),
      );
    },
  );

  // GET /api/admin/received-invoices/:id/file
  fastify.get<{ Params: { id: string } }>(
    "/:id/file",
    { preHandler: requirePermission("invoices.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const invoice = await prisma.received_invoices.findUnique({
        where: { id },
        select: {
          file_name: true,
          file_mime: true,
          year: true,
          month: true,
        },
      });
      if (!invoice?.file_name) return error(reply, "Soubor nenalezen", 404);

      const relPath = nasInvoicesManager.buildReceivedPath(
        invoice.file_name,
        invoice.year,
        invoice.month,
      );
      const nasFile = nasInvoicesManager.readReceived(relPath);
      if (!nasFile) return error(reply, "Soubor na NAS nenalezen", 404);

      const mime = invoice.file_mime || "application/pdf";
      // "inline": the FE (ReceivedInvoices.openFile) opens this in a new
      // window for viewing, not as a forced download.
      return reply
        .type(mime)
        .header(
          "Content-Disposition",
          contentDisposition("inline", invoice.file_name),
        )
        .send(nasFile.data);
    },
  );

  fastify.get<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("invoices.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const invoice = await prisma.received_invoices.findUnique({
        where: { id },
      });
      if (!invoice) return error(reply, "Přijatá faktura nenalezena", 404);
      return success(reply, {
        ...invoice,
        has_file: !!invoice.file_name,
      });
    },
  );

  fastify.post(
    "/",
    { preHandler: requirePermission("invoices.create") },
    async (request, reply) => {
      const contentType = request.headers["content-type"] || "";

      // Multipart upload: files[] + invoices JSON metadata
      if (contentType.includes("multipart/form-data")) {
        const parts = request.parts();
        const files: Array<{
          data: Buffer;
          name: string;
          mime: string;
          size: number;
        }> = [];
        let invoicesMeta: Array<Record<string, unknown>> = [];

        for await (const part of parts) {
          if (part.type === "file") {
            const buf = await part.toBuffer();
            files.push({
              data: buf,
              name: part.filename || "file",
              mime: part.mimetype || "application/octet-stream",
              size: buf.length,
            });
          } else if (part.fieldname === "invoices") {
            try {
              const parsed: unknown = JSON.parse(part.value as string);
              // The frontend sends a partial payload (one element per uploaded
              // file, only the editable fields). Validate each entry against a
              // permissive variant of the create schema so untyped keys (NaN,
              // huge numbers, weird date strings) can't reach the
              // calculations below.
              const arrSchema = z.array(CreateReceivedInvoiceSchema.partial());
              const validated = arrSchema.safeParse(parsed);
              if (validated.success) {
                invoicesMeta = validated.data as Array<Record<string, unknown>>;
              } else {
                return error(
                  reply,
                  "Neplatná metadata faktur: " +
                    validated.error.issues.map((i) => i.message).join(", "),
                  400,
                );
              }
            } catch {
              return error(reply, "Neplatný JSON v poli 'invoices'", 400);
            }
          }
        }

        if (files.length === 0)
          return error(reply, "Vyberte alespoň jeden soubor", 400);

        const now = new Date();
        const createdIds: number[] = [];

        const useNas = nasInvoicesManager.isConfigured();

        for (let i = 0; i < files.length; i++) {
          const file = files[i];
          const meta = invoicesMeta[i] || {};
          const amount = Number(meta.amount ?? 0);
          const vatRate = Number(meta.vat_rate ?? 21);
          // `amount` is the GROSS total (VAT included); VAT is the portion within it.
          const vatAmount = vatFromGross(amount, vatRate);

          const issueDate = meta.issue_date
            ? new Date(String(meta.issue_date))
            : null;
          const invoiceMonth = issueDate
            ? issueDate.getMonth() + 1
            : Number(meta.month) || now.getMonth() + 1;
          const invoiceYear = issueDate
            ? issueDate.getFullYear()
            : Number(meta.year) || now.getFullYear();

          if (!useNas) {
            return error(reply, "NAS úložiště není nakonfigurováno", 503);
          }

          const nasResult = nasInvoicesManager.saveReceived(
            file.name,
            invoiceYear,
            invoiceMonth,
            file.data,
          );
          if ("error" in nasResult) {
            return error(reply, nasResult.error, 503);
          }

          const invoice = await prisma.received_invoices.create({
            data: {
              month: invoiceMonth,
              year: invoiceYear,
              supplier_name: meta.supplier_name
                ? String(meta.supplier_name)
                : file.name,
              invoice_number: meta.invoice_number
                ? String(meta.invoice_number)
                : null,
              description: meta.description ? String(meta.description) : null,
              amount,
              currency: meta.currency ? String(meta.currency) : "CZK",
              vat_rate: vatRate,
              vat_amount: vatAmount,
              issue_date: meta.issue_date
                ? new Date(String(meta.issue_date))
                : null,
              due_date: meta.due_date ? new Date(String(meta.due_date)) : null,
              status: "unpaid",
              notes: meta.notes ? String(meta.notes) : null,
              uploaded_by: request.authData?.userId,
              file_name: nasResult.filePath
                ? path.basename(nasResult.filePath)
                : file.name,
              file_mime: file.mime,
              file_size: file.size,
            },
          });
          createdIds.push(invoice.id);
        }

        await logAudit({
          request,
          authData: request.authData,
          action: "create",
          entityType: "invoice",
          entityId: createdIds[0],
          description: `Nahráno ${createdIds.length} přijatých faktur`,
        });
        return success(
          reply,
          { ids: createdIds, count: createdIds.length },
          201,
          `Nahráno ${createdIds.length} faktur`,
        );
      }

      // JSON body: single invoice creation (no file)
      const parsed = parseBody(CreateReceivedInvoiceSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const status = body.status;
      if (!VALID_STATUSES.includes(status as (typeof VALID_STATUSES)[number])) {
        return error(reply, "Neplatný stav", 400);
      }

      const amount = body.amount;
      const vatRate = body.vat_rate;
      const invoice = await prisma.received_invoices.create({
        data: {
          month: Number(body.month),
          year: Number(body.year),
          supplier_name: String(body.supplier_name),
          invoice_number: body.invoice_number
            ? String(body.invoice_number)
            : null,
          description: body.description ? String(body.description) : null,
          amount,
          currency: body.currency ? String(body.currency) : "CZK",
          vat_rate: vatRate,
          // `amount` is the GROSS total (VAT included); VAT is the portion
          // within it — consistent with the multipart create and update paths.
          vat_amount: vatFromGross(amount, vatRate),
          issue_date: body.issue_date
            ? new Date(String(body.issue_date))
            : null,
          due_date: body.due_date ? new Date(String(body.due_date)) : null,
          status: status as received_invoices_status,
          notes: body.notes ? String(body.notes) : null,
          uploaded_by: request.authData?.userId,
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "invoice",
        entityId: invoice.id,
        description: `Vytvořena přijatá faktura od ${invoice.supplier_name}`,
      });
      return success(reply, { id: invoice.id }, 201, "Faktura byla vytvořena");
    },
  );

  fastify.put<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("invoices.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const parsed = parseBody(UpdateReceivedInvoiceSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.received_invoices.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);

      if (body.status !== undefined) {
        const status = String(body.status);
        if (
          !VALID_STATUSES.includes(status as (typeof VALID_STATUSES)[number])
        ) {
          return error(reply, "Neplatný stav", 400);
        }
        // Prevent reverting paid status (matching PHP)
        if (String(existing.status) === "paid" && status !== "paid") {
          return error(reply, "Nelze vrátit stav uhrazené faktury", 400);
        }
      }

      if (String(existing.status) === "paid") {
        const attempted = Object.keys(body).filter(
          (k) => !["status", "paid_date", "notes"].includes(k),
        );
        if (attempted.length > 0) {
          return error(reply, "Nelze upravit uhrazenou fakturu", 400);
        }
      }

      // Recalculate vat_amount when amount (gross) or vat_rate changes.
      const finalAmount =
        body.amount !== undefined
          ? Number(body.amount)
          : Number(existing.amount);
      const finalVatRate =
        body.vat_rate !== undefined
          ? Number(body.vat_rate)
          : Number(existing.vat_rate);
      // `amount` is the GROSS total (VAT included); VAT is the portion within it.
      const computedVat = vatFromGross(finalAmount, finalVatRate);

      // Auto-set paid_date when status transitions to paid (matching PHP).
      // paid_date is @db.Date (truncated to the UTC date part) —
      // utcMidnightOfLocalDay keeps the LOCAL calendar day even during the
      // 00:00–02:00 Prague window (a bare new Date() stored yesterday there).
      const newStatus =
        body.status !== undefined
          ? String(body.status)
          : String(existing.status);
      const paidDate =
        newStatus === "paid" && String(existing.status) !== "paid"
          ? utcMidnightOfLocalDay()
          : body.paid_date !== undefined
            ? body.paid_date
              ? new Date(String(body.paid_date))
              : null
            : undefined;

      // Auto-update month/year from issue_date if issue_date changes (matching PHP)
      let autoMonth = body.month !== undefined ? Number(body.month) : undefined;
      let autoYear = body.year !== undefined ? Number(body.year) : undefined;
      if (body.issue_date && !body.month && !body.year) {
        const issueDate = new Date(String(body.issue_date));
        if (!isNaN(issueDate.getTime())) {
          autoMonth = issueDate.getMonth() + 1;
          autoYear = issueDate.getFullYear();
        }
      }

      await prisma.received_invoices.update({
        where: { id },
        data: {
          supplier_name:
            body.supplier_name !== undefined
              ? String(body.supplier_name)
              : undefined,
          invoice_number:
            body.invoice_number !== undefined
              ? body.invoice_number
                ? String(body.invoice_number)
                : null
              : undefined,
          description:
            body.description !== undefined
              ? body.description
                ? String(body.description)
                : null
              : undefined,
          amount: body.amount !== undefined ? Number(body.amount) : undefined,
          currency:
            body.currency !== undefined ? String(body.currency) : undefined,
          vat_rate:
            body.vat_rate !== undefined ? Number(body.vat_rate) : undefined,
          vat_amount:
            body.amount !== undefined || body.vat_rate !== undefined
              ? computedVat
              : body.vat_amount !== undefined
                ? Number(body.vat_amount)
                : undefined,
          issue_date:
            body.issue_date !== undefined
              ? body.issue_date
                ? new Date(String(body.issue_date))
                : null
              : undefined,
          due_date:
            body.due_date !== undefined
              ? body.due_date
                ? new Date(String(body.due_date))
                : null
              : undefined,
          paid_date: paidDate,
          status:
            body.status !== undefined
              ? (String(body.status) as received_invoices_status)
              : undefined,
          notes:
            body.notes !== undefined
              ? body.notes
                ? String(body.notes)
                : null
              : undefined,
          month: autoMonth,
          year: autoYear,
          modified_at: new Date(),
        },
      });
      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "invoice",
        entityId: id,
        description: `Upravena přijatá faktura`,
      });
      return success(reply, { id }, 200, "Faktura byla uložena");
    },
  );

  fastify.delete<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("invoices.delete") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const existing = await prisma.received_invoices.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);

      // Delete DB record first, then NAS file — avoids orphaned file if DB delete fails
      await prisma.received_invoices.delete({ where: { id } });

      if (existing.file_name) {
        const relPath = nasInvoicesManager.buildReceivedPath(
          existing.file_name,
          existing.year,
          existing.month,
        );
        nasInvoicesManager.deleteReceived(relPath);
      }
      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "invoice",
        entityId: id,
        description: `Smazána přijatá faktura`,
      });
      return success(reply, null, 200, "Přijatá faktura smazána");
    },
  );
}
