import prisma from "../config/database";
import { utcMidnightOfLocalDay } from "../utils/date";
import { toCzk } from "./exchange-rates";
import {
  generateInvoiceNumber,
  releaseInvoiceNumber,
  assignInvoiceNumber,
} from "./numbering.service";

// Status transition rules matching PHP.
// draft -> issued is the finalize step (consumes + assigns the invoice number).
const VALID_TRANSITIONS: Record<string, string[]> = {
  draft: ["issued"],
  issued: ["paid"],
  overdue: ["paid"],
  paid: [],
};

const ALLOWED_SORT_FIELDS = [
  "id",
  "invoice_number",
  "status",
  "issue_date",
  "due_date",
  "currency",
];

interface InvoiceItemInput {
  description?: string | null;
  item_description?: string | null;
  quantity?: number;
  unit?: string | null;
  unit_price?: number;
  vat_rate?: number;
  position?: number;
}

interface InvoiceSectionInput {
  title?: string | null;
  title_cz?: string | null;
  content?: string | null;
}

/**
 * Body shape accepted by createInvoice/updateInvoice. All fields optional and
 * loosely typed because the payload arrives validated-but-coerced from the Zod
 * route layer (numbers may still be string-from-form). Kept permissive so the
 * existing String()/Number() coercions below stay correct; replaces the old
 * `Record<string, any>`.
 */
export interface InvoiceInput {
  invoice_number?: string | number | null;
  order_id?: number | string | null;
  customer_id?: number | string | null;
  status?: string;
  currency?: string;
  vat_rate?: number | string | null;
  apply_vat?: boolean | number | string;
  payment_method?: string | null;
  constant_symbol?: string | null;
  bank_name?: string | null;
  bank_swift?: string | null;
  bank_iban?: string | null;
  bank_account?: string | null;
  issue_date?: string | null;
  due_date?: string | null;
  tax_date?: string | null;
  issued_by?: string | null;
  billing_text?: string | null;
  language?: string;
  internal_notes?: string | null;
  paid_date?: string | null;
  items?: InvoiceItemInput[];
  sections?: InvoiceSectionInput[];
  [key: string]: unknown;
}

/**
 * Single rounding-correct money/VAT core. All three former implementations
 * (computeInvoiceTotals, invoiceTotalWithVat, the stats VAT loop) funnel through
 * this so the subtotal/accumulation/rounding logic cannot diverge again.
 *
 * Each caller resolves its own per-line VAT *rate* before calling (the three
 * paths historically differ in how a line rate of 0 is treated — that nuance
 * is preserved by keeping rate resolution caller-side via `resolveRate`).
 *
 * - `subtotal` = Σ(qty × unit_price) over lines.
 * - `vat` = Σ of per-line VAT. When `roundPerLine` is true each line's VAT is
 *   rounded to 2 decimals BEFORE accumulation (invoice totals); when false the
 *   raw per-line VAT is accumulated and the caller rounds the sum (stats).
 *
 * Numeric outputs are byte-for-byte identical to the previous three code paths.
 */
function computeMoney<T>(
  lines: T[],
  applyVat: boolean | null,
  getBase: (line: T) => number,
  resolveRate: (line: T) => number,
  roundPerLine: boolean,
): { subtotal: number; vat: number } {
  let subtotal = 0;
  let vat = 0;
  for (const line of lines) {
    const base = getBase(line);
    subtotal += base;
    if (applyVat) {
      const lineVat = base * (resolveRate(line) / 100);
      vat += roundPerLine ? Math.round(lineVat * 100) / 100 : lineVat;
    }
  }
  return { subtotal, vat };
}

interface InvoiceFilterParams {
  search?: string;
  status?: string;
  customer_id?: number;
  month?: number;
  year?: number;
}

interface ListInvoicesParams extends InvoiceFilterParams {
  page: number;
  limit: number;
  skip: number;
  sort: string;
  order: "asc" | "desc";
  search: string;
}

export interface CurrencyAmount {
  amount: number;
  currency: string;
}

/**
 * Shared `where` builder for the issued-invoices list and the per-currency
 * list-totals aggregation, so both stay in sync (any new filter applies to
 * both). month/year filter on issue_date (matching the list).
 */
function buildInvoiceWhere(
  params: InvoiceFilterParams,
): Record<string, unknown> {
  const { status, customer_id, month, year, search } = params;
  const where: Record<string, unknown> = {};
  if (status) where.status = status;
  if (customer_id) where.customer_id = customer_id;
  if (month && year) {
    // issue_date is @db.Date: Prisma compares by UTC date part, so the month
    // boundaries must be UTC midnights. Local midnights shifted the window a
    // day back — the filter included the previous month's last day and
    // DROPPED invoices issued on the selected month's last day.
    const from = new Date(Date.UTC(year, month - 1, 1));
    const to = new Date(Date.UTC(year, month, 1));
    where.issue_date = { gte: from, lt: to };
  }
  if (search) {
    where.OR = [
      { invoice_number: { contains: search } },
      { customers: { name: { contains: search } } },
      { customers: { company_id: { contains: search } } },
    ];
  }
  return where;
}

function computeInvoiceTotals(
  items: Array<{ quantity: unknown; unit_price: unknown; vat_rate: unknown }>,
  applyVat: boolean | null,
  defaultVatRate: unknown,
) {
  const { subtotal, vat: vatAmount } = computeMoney(
    items,
    applyVat,
    (i) => (Number(i.quantity) || 0) * (Number(i.unit_price) || 0),
    // Preserve original semantics: a line vat_rate of 0 (non-null, non-"") IS used.
    (i) =>
      i.vat_rate != null && i.vat_rate !== ""
        ? Number(i.vat_rate)
        : defaultVatRate != null
          ? Number(defaultVatRate)
          : 21,
    true, // round each line's VAT before accumulation
  );
  return {
    subtotal: Math.round(subtotal * 100) / 100,
    vat_amount: Math.round(vatAmount * 100) / 100,
    total: Math.round((subtotal + vatAmount) * 100) / 100,
  };
}

export async function markOverdueInvoices() {
  try {
    // due_date is @db.Date (UTC midnight of the calendar day). Compare
    // against UTC midnight of the LOCAL today — a bare new Date() has
    // yesterday's UTC date during the 00:00–02:00 Prague window, so the
    // overdue flip lagged a day there. Due TODAY = not yet overdue.
    const today = utcMidnightOfLocalDay();
    await prisma.invoices.updateMany({
      where: { status: "issued", due_date: { lt: today } },
      data: { status: "overdue" },
    });
    // Reverse: if due_date was changed to today/future, set back to issued
    await prisma.invoices.updateMany({
      where: { status: "overdue", due_date: { gte: today } },
      data: { status: "issued" },
    });
  } catch (err) {
    console.error("markOverdueInvoices failed:", err);
  }
}

export async function listInvoices(params: ListInvoicesParams) {
  const { page, limit, skip, sort, order } = params;
  const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";

  const where = buildInvoiceWhere(params);

  const orderBy: Record<string, string> = { [sortField]: order };

  const [invoices, total] = await Promise.all([
    prisma.invoices.findMany({
      where,
      skip,
      take: limit,
      orderBy,
      include: {
        customers: { select: { id: true, name: true } },
        invoice_items: true,
        orders: { select: { id: true, order_number: true } },
      },
    }),
    prisma.invoices.count({ where }),
  ]);

  const enriched = invoices.map((inv) => {
    const totals = computeInvoiceTotals(
      inv.invoice_items,
      inv.apply_vat,
      inv.vat_rate,
    );
    const { invoice_items, ...rest } = inv;
    return {
      ...rest,
      items: invoice_items,
      customer_name: inv.customers?.name || null,
      order_number: inv.orders?.order_number || null,
      ...totals,
    };
  });

  return { data: enriched, total, page, limit };
}

/**
 * Sum issued-invoice TOTAL (incl. VAT) per currency across the WHOLE filtered
 * set (not a single page). Reuses `buildInvoiceWhere` so the filters track the
 * list, and `computeInvoiceTotals` so the per-invoice math matches the
 * list/detail exactly. Returns one entry per currency, rounded to 2dp,
 * zero/empty totals dropped.
 *
 * NOTE: This is SEPARATE from `getInvoiceStats` (the monthly KPI cards). That
 * function is untouched — this only sums the list's visible rows per currency.
 */
export async function getInvoiceListTotals(
  params: InvoiceFilterParams,
): Promise<{ totals: CurrencyAmount[] }> {
  const where = buildInvoiceWhere(params);

  const invoices = await prisma.invoices.findMany({
    where,
    include: { invoice_items: true },
  });

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

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

  return { totals };
}

export {
  generateInvoiceNumber as getNextInvoiceNumberFormatted,
  previewInvoiceNumber as getNextInvoiceNumberPreview,
} from "./numbering.service";

export function invoiceTotalWithVat(inv: {
  apply_vat: boolean | null;
  vat_rate: { toNumber(): number } | null;
  currency: string | null;
  invoice_items: Array<{
    quantity: { toNumber(): number } | null;
    unit_price: { toNumber(): number } | null;
    vat_rate: { toNumber(): number } | null;
  }>;
}) {
  const { subtotal, vat } = computeMoney(
    inv.invoice_items,
    inv.apply_vat,
    (i) =>
      (Number(i.quantity?.toNumber()) || 0) *
      (Number(i.unit_price?.toNumber()) || 0),
    // Preserve original `||` semantics: a line rate of 0/NaN falls through to
    // the invoice default, then 21.
    (i) =>
      Number(i.vat_rate?.toNumber()) || Number(inv.vat_rate?.toNumber()) || 21,
    true, // round each line's VAT before accumulation
  );
  return subtotal + vat;
}

export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
  const now = new Date();
  const year = queryYear || now.getFullYear();
  const month = queryMonth || now.getMonth() + 1;

  // issue_date is @db.Date (compared by UTC date part) → UTC-midnight period
  // boundaries, half-open [gte, lt) ranges. Local-midnight bounds included
  // the previous period's boundary day in the stats.
  const monthStart = new Date(Date.UTC(year, month - 1, 1));
  const nextMonthStart = new Date(Date.UTC(year, month, 1));
  const startOfYear = new Date(Date.UTC(year, 0, 1));
  const startOfNextYear = new Date(Date.UTC(year + 1, 0, 1));

  const [monthInvoices, awaitingInvoices, overdueInvoices] = await Promise.all([
    prisma.invoices.findMany({
      where: {
        // Drafts are not real financial documents — exclude them so their
        // VAT/amounts never inflate the monthly stats (vat_month etc.).
        status: { not: "draft" },
        issue_date: { gte: monthStart, lt: nextMonthStart },
      },
      include: { invoice_items: true },
    }),
    prisma.invoices.findMany({
      where: {
        status: "issued",
        issue_date: { gte: startOfYear, lt: startOfNextYear },
      },
      include: { invoice_items: true },
    }),
    prisma.invoices.findMany({
      where: {
        status: "overdue",
        issue_date: { gte: startOfYear, lt: startOfNextYear },
      },
      include: { invoice_items: true },
    }),
  ]);

  const aggregateByCurrency = (
    invoices: Parameters<typeof invoiceTotalWithVat>[0][],
  ) => {
    const map: Record<string, number> = {};
    for (const inv of invoices) {
      const cur = inv.currency || "CZK";
      map[cur] = (map[cur] || 0) + invoiceTotalWithVat(inv);
    }
    return Object.entries(map)
      .filter(([, v]) => v > 0)
      .map(([currency, amount]) => ({
        amount: Math.round(amount * 100) / 100,
        currency,
      }));
  };

  const sumCzk = async (
    invoices: Parameters<typeof invoiceTotalWithVat>[0][],
  ) => {
    let total = 0;
    for (const inv of invoices) {
      const amount = invoiceTotalWithVat(inv);
      total += await toCzk(amount, inv.currency || "CZK");
    }
    return Math.round(total * 100) / 100;
  };

  const paidInvoices = monthInvoices.filter((i) => i.status === "paid");

  // VAT for the month, per currency. Uses the shared money core with
  // roundPerLine=false (the stats path historically accumulates RAW per-line
  // VAT and rounds only the final sum — preserved byte-for-byte).
  const vatMap: Record<string, number> = {};
  for (const inv of monthInvoices) {
    if (!inv.apply_vat) continue;
    const cur = inv.currency || "CZK";
    const { vat } = computeMoney(
      inv.invoice_items,
      inv.apply_vat,
      (item) => (Number(item.quantity) || 0) * (Number(item.unit_price) || 0),
      (item) => Number(item.vat_rate) || Number(inv.vat_rate) || 21,
      false, // accumulate raw per-line VAT; round only the per-currency sum
    );
    vatMap[cur] = (vatMap[cur] || 0) + vat;
  }
  const vatAmounts = Object.entries(vatMap)
    .filter(([, v]) => v > 0)
    .map(([currency, amount]) => ({
      amount: Math.round(amount * 100) / 100,
      currency,
    }));

  // VAT also needs CZK conversion — run each currency's toCzk concurrently.
  const vatCzkConverted = (
    await Promise.all(
      Object.entries(vatMap).map(([cur, amount]) => toCzk(amount, cur)),
    )
  ).reduce((s, v) => s + v, 0);

  // These aggregates/conversions are independent — run them concurrently
  // instead of serially awaiting each in the return literal.
  const [paidMonthCzk, awaitingCzk, overdueCzk] = await Promise.all([
    sumCzk(paidInvoices),
    sumCzk(awaitingInvoices),
    sumCzk(overdueInvoices),
  ]);

  return {
    paid_month: aggregateByCurrency(paidInvoices),
    paid_month_czk: paidMonthCzk,
    paid_month_count: paidInvoices.length,
    awaiting: aggregateByCurrency(awaitingInvoices),
    awaiting_czk: awaitingCzk,
    awaiting_count: awaitingInvoices.length,
    overdue: aggregateByCurrency(overdueInvoices),
    overdue_czk: overdueCzk,
    overdue_count: overdueInvoices.length,
    vat_month: vatAmounts,
    vat_month_czk: Math.round(vatCzkConverted * 100) / 100,
    month,
    year,
  };
}

export async function getOrderDataForInvoice(orderId: number) {
  const order = await prisma.orders.findUnique({
    where: { id: orderId },
    // This result is spread into the order-data API response — never include
    // the PO attachment blob (served only by GET /orders/:id/attachment).
    omit: { attachment_data: true },
    include: {
      customers: true,
      order_items: { orderBy: { position: "asc" } },
    },
  });
  if (!order) return null;
  const { order_items, customers, ...rest } = order;
  return {
    ...rest,
    items: order_items,
    customer_name: customers?.name || null,
  };
}

// NOTE: returns `null` (not `{ error, status }`) on not-found — intentional and
// load-bearing: routes/admin/invoices.ts checks `if (!invoice) 404`. Returning a
// truthy `{ error }` object here would bypass that 404 guard, so this stays null.
export async function getInvoice(id: number) {
  const invoice = await prisma.invoices.findUnique({
    where: { id },
    include: {
      customers: true,
      invoice_items: { orderBy: { position: "asc" } },
      // id tiebreak: position can repeat, keep the order deterministic.
      invoice_sections: { orderBy: [{ position: "asc" }, { id: "asc" }] },
      orders: { select: { id: true, order_number: true } },
    },
  });
  if (!invoice) return null;
  const { invoice_items, invoice_sections, ...rest } = invoice;
  return {
    ...rest,
    items: invoice_items,
    sections: invoice_sections,
    customer: invoice.customers,
    customer_name: invoice.customers?.name || null,
    order_number: invoice.orders?.order_number || null,
    valid_transitions: VALID_TRANSITIONS[invoice.status as string] || [],
  };
}

export async function createInvoice(body: InvoiceInput) {
  const status = body.status ? String(body.status) : "draft";

  // Deferred numbering: a draft carries NO invoice_number (the column is
  // nullable-unique so many drafts coexist). The official number is consumed
  // only when the draft is finalized (assignInvoiceNumber on draft->issued).
  // A caller-supplied number, or an invoice created already-finalized, numbers now.
  const explicit =
    body.invoice_number !== undefined && body.invoice_number !== null
      ? String(body.invoice_number)
      : null;
  const invoiceNumber =
    explicit ??
    (status === "draft" ? null : (await generateInvoiceNumber()).number);

  // Header + items + sections are ONE write — a failure mid-way must not
  // leave a headerless/partial invoice behind.
  const invoice = await prisma.$transaction(async (tx) => {
    const created = await tx.invoices.create({
      data: {
        invoice_number: invoiceNumber,
        order_id: body.order_id ? Number(body.order_id) : null,
        customer_id: body.customer_id ? Number(body.customer_id) : null,
        status,
        currency: body.currency ? String(body.currency) : "CZK",
        vat_rate: body.vat_rate != null ? Number(body.vat_rate) : 21.0,
        apply_vat: body.apply_vat !== false,
        payment_method: body.payment_method
          ? String(body.payment_method)
          : null,
        constant_symbol: body.constant_symbol
          ? String(body.constant_symbol)
          : null,
        bank_name: body.bank_name ? String(body.bank_name) : null,
        bank_swift: body.bank_swift ? String(body.bank_swift) : null,
        bank_iban: body.bank_iban ? String(body.bank_iban) : null,
        bank_account: body.bank_account ? String(body.bank_account) : null,
        issue_date: body.issue_date ? new Date(String(body.issue_date)) : null,
        due_date: body.due_date ? new Date(String(body.due_date)) : null,
        tax_date: body.tax_date ? new Date(String(body.tax_date)) : null,
        issued_by: body.issued_by ? String(body.issued_by) : null,
        billing_text: body.billing_text ? String(body.billing_text) : null,
        language: body.language ? String(body.language) : "cs",
        internal_notes: body.internal_notes
          ? String(body.internal_notes)
          : null,
      },
    });

    if (Array.isArray(body.items)) {
      await tx.invoice_items.createMany({
        data: body.items.map((item, i) => ({
          invoice_id: created.id,
          description: item.description ?? null,
          item_description: item.item_description ?? null,
          quantity: item.quantity ?? 1,
          unit: item.unit ?? null,
          unit_price: item.unit_price ?? 0,
          vat_rate: item.vat_rate ?? 21.0,
          position: item.position ?? i,
        })),
      });
    }

    if (Array.isArray(body.sections)) {
      await tx.invoice_sections.createMany({
        data: body.sections.map((s, i) => ({
          invoice_id: created.id,
          title: s.title ?? null,
          title_cz: s.title_cz ?? null,
          content: s.content ?? null,
          position: i,
        })),
      });
    }

    return created;
  });

  return invoice;
}

export async function updateInvoice(id: number, body: InvoiceInput) {
  const existing = await prisma.invoices.findUnique({ where: { id } });
  if (!existing) return { error: "not_found" as const };

  const currentStatus = existing.status as string;

  // Handle status transition
  if (body.status !== undefined && body.status !== currentStatus) {
    const newStatus = String(body.status);
    const allowed = VALID_TRANSITIONS[currentStatus] || [];
    if (!allowed.includes(newStatus)) {
      return { error: "invalid_transition" as const, currentStatus, newStatus };
    }
  }

  const data: Record<string, unknown> = { modified_at: new Date() };

  // Allow full editing in 'draft', 'issued' and 'overdue' states.
  const editable =
    currentStatus === "draft" ||
    currentStatus === "issued" ||
    currentStatus === "overdue";
  if (editable) {
    const strFields = [
      "currency",
      "payment_method",
      "constant_symbol",
      "bank_name",
      "bank_swift",
      "bank_iban",
      "bank_account",
      "issued_by",
      "billing_text",
      "language",
    ];
    for (const f of strFields) {
      if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
    }
    if (body.customer_id !== undefined)
      data.customer_id = body.customer_id ? Number(body.customer_id) : null;
    if (body.vat_rate !== undefined) data.vat_rate = Number(body.vat_rate);
    if (body.apply_vat !== undefined)
      data.apply_vat =
        body.apply_vat === true ||
        body.apply_vat === 1 ||
        body.apply_vat === "1";
    if (body.issue_date !== undefined)
      data.issue_date = body.issue_date
        ? new Date(String(body.issue_date))
        : null;
    if (body.due_date !== undefined)
      data.due_date = body.due_date ? new Date(String(body.due_date)) : null;
    if (body.tax_date !== undefined)
      data.tax_date = body.tax_date ? new Date(String(body.tax_date)) : null;
  }

  // Internal notes editable in draft/issued/overdue (never printed)
  if (editable) {
    if (body.internal_notes !== undefined)
      data.internal_notes = body.internal_notes
        ? String(body.internal_notes)
        : null;
  }

  // Status change
  if (body.status !== undefined) {
    data.status = String(body.status);
    // Auto-set paid_date when transitioning to paid. 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.
    if (String(body.status) === "paid" && !existing.paid_date) {
      data.paid_date = utcMidnightOfLocalDay();
    }
  }

  if (body.paid_date !== undefined && currentStatus !== "paid")
    data.paid_date = body.paid_date ? new Date(String(body.paid_date)) : null;

  // Finalize transition draft -> issued: consume + write the official invoice
  // number ATOMICALLY with the status change, so there is never a window where
  // the invoice is "issued" but still has a null invoice_number.
  // assignInvoiceNumber is idempotent (won't re-number an already-numbered row).
  const finalizing =
    currentStatus === "draft" &&
    body.status !== undefined &&
    String(body.status) === "issued";

  let assignedNumber: string | null = null;
  if (finalizing) {
    await prisma.$transaction(async (tx) => {
      await tx.invoices.update({ where: { id }, data });
      assignedNumber = await assignInvoiceNumber(id, tx);
    });
  } else {
    await prisma.invoices.update({ where: { id }, data });
  }

  // Allow items/sections full-replace while editable (draft/issued/overdue) —
  // both in ONE transaction so a failure can't leave a half-replaced document.
  const replaceItems = editable && Array.isArray(body.items);
  const replaceSections = editable && Array.isArray(body.sections);
  if (replaceItems || replaceSections) {
    await prisma.$transaction(async (tx) => {
      if (replaceItems) {
        await tx.invoice_items.deleteMany({ where: { invoice_id: id } });
        await tx.invoice_items.createMany({
          data: body.items!.map((item, i) => ({
            invoice_id: id,
            description: item.description ?? null,
            item_description: item.item_description ?? null,
            quantity: item.quantity ?? 1,
            unit: item.unit ?? null,
            unit_price: item.unit_price ?? 0,
            vat_rate: item.vat_rate ?? 21.0,
            position: item.position ?? i,
          })),
        });
      }
      if (replaceSections) {
        await tx.invoice_sections.deleteMany({ where: { invoice_id: id } });
        await tx.invoice_sections.createMany({
          data: body.sections!.map((s, i) => ({
            invoice_id: id,
            title: s.title ?? null,
            title_cz: s.title_cz ?? null,
            content: s.content ?? null,
            position: i,
          })),
        });
      }
    });
  }

  // Reflect the number the finalize transition just assigned (existing was read
  // before the assign, so its invoice_number is still the pre-finalize null).
  return {
    id,
    invoice_number: assignedNumber ?? existing.invoice_number,
  };
}

export async function deleteInvoice(id: number) {
  const existing = await prisma.invoices.findUnique({ where: { id } });
  if (!existing) return null;

  await prisma.invoices.delete({ where: { id } });

  // Derive the sequence year from the invoice number ("<seq>/<YYYY>"). Guard the
  // split: a non-standard number (no "/" or a non-4-digit / non-numeric part)
  // falls back to the current year rather than releasing a bogus sequence.
  const yearPart = existing.invoice_number?.split("/")[1];
  const parsedYear =
    yearPart && /^\d{4}$/.test(yearPart) ? Number(yearPart) : NaN;
  const year = Number.isNaN(parsedYear) ? new Date().getFullYear() : parsedYear;
  await releaseInvoiceNumber(year, existing.invoice_number ?? undefined);

  return existing;
}
