import React from "react";
import { renderToBuffer } from "@react-pdf/renderer";
import type { AdminApiContext } from "@shopify/shopify-app-react-router/server";

import db from "../../db.server";
import { composeInvoice } from "./composeInvoice";
import {
  generateInvoice,
  sanitiseForFilename,
  toOrderGid,
  uploadPdfToShopifyFiles,
  writeOrderMetafields,
  type GeneratedInvoice,
} from "./generateInvoice.server";
import { loadOrderForInvoice } from "./loadOrderForInvoice.server";
import { getLogoDataUrl } from "./logoCache.server";
import { InvoiceDocument } from "./pdf/InvoiceDocument";

export interface CancelAndReissueArgs {
  shopDomain: string;
  admin: AdminApiContext;
  orderId: string;
}

export interface CancelAndReissueResult {
  storno: {
    invoiceId: string;
    invoiceNumber: string;
    pdfUrl: string;
  };
  newInvoice: GeneratedInvoice;
}

/**
 * Cancels the latest sent invoice for an order by issuing a Stornorechnung
 * (negative amounts, references the original number) and then issuing a
 * brand-new invoice with a fresh number reflecting the corrected data.
 *
 * Both documents are uploaded to Shopify Files. The original invoice row is
 * marked `cancelledAt = now()`, the storno is persisted as
 * `Invoice { kind: 'storno', cancelsInvoiceId: <original.id> }`, and the
 * order's metafields are updated to point at the new invoice (with the
 * storno PDF URL written to a separate metafield).
 */
export async function cancelAndReissue(
  args: CancelAndReissueArgs,
): Promise<CancelAndReissueResult> {
  const { shopDomain, admin } = args;
  const orderGid = toOrderGid(args.orderId);

  const settings = await db.shopSettings.upsert({
    where: { shopDomain },
    update: {},
    create: { shopDomain },
  });

  const original = await db.invoice.findFirst({
    where: {
      shopDomain,
      orderId: orderGid,
      kind: "invoice",
      cancelledAt: null,
    },
    orderBy: [{ version: "desc" }, { createdAt: "desc" }],
  });

  if (!original) {
    throw new Error("No active invoice found for this order to cancel.");
  }

  const order = await loadOrderForInvoice(admin, orderGid);

  // Storno number: same as original with a "-S" suffix (so it is visually
  // tied to the cancelled invoice and never collides with the new number).
  const stornoNumber = `${original.invoiceNumber}-S`;

  const stornoView = composeInvoice({
    order,
    settings,
    invoiceNumber: stornoNumber,
    storno: { cancelsNumber: original.invoiceNumber },
  });

  const logoDataUrl = await getLogoDataUrl(shopDomain, settings.logoUrl);
  if (logoDataUrl) stornoView.issuer.logoDataUrl = logoDataUrl;

  const stornoBuffer = (await renderToBuffer(
    <InvoiceDocument invoice={stornoView} />,
  )) as Buffer;

  const stornoUpload = await uploadPdfToShopifyFiles(admin, {
    bytes: stornoBuffer,
    filename: `Stornorechnung-${sanitiseForFilename(stornoNumber)}.pdf`,
    alt: `Stornorechnung ${stornoNumber}`,
  });

  const stornoRow = await db.$transaction(async (tx) => {
    const created = await tx.invoice.create({
      data: {
        shopDomain,
        orderId: orderGid,
        orderName: order.name,
        orderNumber: order.orderNumber,
        invoiceNumber: stornoNumber,
        language: stornoView.language,
        kind: "storno",
        version: 1,
        cancelsInvoiceId: original.id,
        pdfFileGid: stornoUpload.fileGid,
        pdfUrl: stornoUpload.url,
        totalsJson: JSON.stringify(stornoView.totals),
        customerJson: JSON.stringify({
          recipient: stornoView.recipient,
          isB2B: stornoView.isB2B,
          recipientVatId: stornoView.recipientVatId,
        }),
        status: "issued",
      },
    });
    await tx.invoice.update({
      where: { id: original.id },
      data: { cancelledAt: new Date(), status: "cancelled" },
    });
    return created;
  });

  // Best-effort: link the storno PDF + previous number on the order.
  try {
    await writeStornoSidecarMetafields(admin, orderGid, {
      stornoUrl: stornoUpload.url,
      previousNumber: original.invoiceNumber,
    });
  } catch (err) {
    console.warn("Storno sidecar metafield write failed:", err);
  }

  // Now issue a brand-new invoice (fresh number from the configured mode).
  const newInvoice = await generateInvoice({ shopDomain, admin, orderId: orderGid });

  // The metafields written by generateInvoice already point at the new
  // invoice's PDF URL/number/version. The storno sidecar metafields above
  // remain referencing the storno PDF and the previous number.

  return {
    storno: {
      invoiceId: stornoRow.id,
      invoiceNumber: stornoRow.invoiceNumber,
      pdfUrl: stornoUpload.url,
    },
    newInvoice,
  };
}

const STORNO_METAFIELDS_MUTATION = `#graphql
  mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields { id namespace key }
      userErrors { field message }
    }
  }
`;

async function writeStornoSidecarMetafields(
  admin: AdminApiContext,
  orderGid: string,
  data: { stornoUrl: string; previousNumber: string },
): Promise<void> {
  const res = await admin.graphql(STORNO_METAFIELDS_MUTATION, {
    variables: {
      metafields: [
        {
          ownerId: orderGid,
          namespace: "linumiq_invoice",
          key: "storno_pdf_url",
          type: "url",
          value: data.stornoUrl,
        },
        {
          ownerId: orderGid,
          namespace: "linumiq_invoice",
          key: "previous_number",
          type: "single_line_text_field",
          value: data.previousNumber,
        },
      ],
    },
  });
  const json = (await res.json()) as {
    data?: {
      metafieldsSet?: {
        userErrors?: { field: string[]; message: string }[];
      };
    };
  };
  const errs = json.data?.metafieldsSet?.userErrors ?? [];
  if (errs.length > 0) {
    throw new Error(`metafieldsSet (storno) failed: ${JSON.stringify(errs)}`);
  }
  // suppress unused-import warnings when the orchestrator path doesn't use this:
  void writeOrderMetafields;
}
