import type { LoaderFunctionArgs } from "react-router";
import { authenticate, unauthenticated } from "../shopify.server";
import db from "../db.server";
import { formatMoney, formatDate, addDays } from "../services/invoice/format";
import { getStrings, pickLanguage } from "../services/invoice/i18n";
import { signGiroCodeUrl } from "../services/invoice/signedUrl";

/**
 * Public endpoint consumed by the checkout / thank-you UI extension AND by
 * the customer-account order page extension to fetch payment instructions
 * (GiroCode + bank details) for an order.
 *
 * Auth: validated Shopify session token. The handler tries
 * `authenticate.public.customerAccount` first and falls back to
 * `authenticate.public.checkout` so a single endpoint serves both surfaces.
 * The shop domain is derived from `sessionToken.dest`; the order id is read
 * from the `?orderId=` query parameter (numeric or GID, both accepted).
 *
 * Returns:
 *   { showPaymentInstructions: boolean, payload?: { ... } }
 *
 * `payload` is only populated when:
 *   - the order has at least one transaction processed by a manual payment
 *     gateway (Shopify's `manualPaymentGateway` flag), and
 *   - the shop has an IBAN configured.
 */
export const loader = async ({ request }: LoaderFunctionArgs) => {
  let sessionToken: { dest?: string } | null = null;
  let cors: <T>(res: T) => T = (r) => r;
  try {
    const auth = await authenticate.public.customerAccount(request);
    sessionToken = auth.sessionToken as { dest?: string };
    cors = auth.cors;
  } catch {
    try {
      const auth = await authenticate.public.checkout(request);
      sessionToken = auth.sessionToken as { dest?: string };
      cors = auth.cors;
    } catch (err) {
      throw err;
    }
  }
  const shop = (sessionToken?.dest ?? "").toString().replace(/^https?:\/\//, "");
  if (!shop) {
    return cors(Response.json({ showPaymentInstructions: false, error: "no-shop" }, { status: 400 }));
  }

  const url = new URL(request.url);
  const orderIdRaw = url.searchParams.get("orderId");
  if (!orderIdRaw) {
    return cors(Response.json({ showPaymentInstructions: false, error: "no-order-id" }, { status: 400 }));
  }
  // The thank-you page exposes the order id as an `OrderIdentity` GID
  // (e.g. `gid://shopify/OrderIdentity/123`). For the Admin API we need an
  // `Order` GID. The numeric id is the same — just rewrite the type segment.
  const numericId = orderIdRaw.replace(/^gid:\/\/shopify\/[^/]+\//, "").replace(/[^0-9]/g, "");
  if (!numericId) {
    return cors(Response.json({ showPaymentInstructions: false, error: "bad-order-id" }, { status: 400 }));
  }
  const orderGid = `gid://shopify/Order/${numericId}`;

  const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
  if (!settings?.iban || !settings.giroCodeEnabled) {
    // No bank details / GiroCode disabled — nothing to render.
    return cors(Response.json({ showPaymentInstructions: false, reason: "no-iban-or-disabled" }));
  }

  let orderInfo: OrderInfo | null = null;
  try {
    const { admin } = await unauthenticated.admin(shop);
    // Brief retry: the Order may not be queryable for a moment after creation.
    let lastErr: unknown = null;
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        orderInfo = await fetchOrderInfo(admin, orderGid);
        if (orderInfo) break;
      } catch (e) {
        lastErr = e;
      }
      await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
    }
    if (!orderInfo && lastErr) throw lastErr;
  } catch (err) {
    const msg = (err as Error)?.message ?? String(err);
    console.warn(`payment-info: failed to load order ${orderGid} for ${shop}:`, err);
    return cors(
      Response.json(
        { showPaymentInstructions: false, error: "order-load-failed", detail: msg.slice(0, 500) },
        { status: 502 },
      ),
    );
  }
  if (!orderInfo || !orderInfo.isManual) {
    return cors(
      Response.json({
        showPaymentInstructions: false,
        reason: "not-manual-payment",
      }),
    );
  }

  const language = pickLanguage(orderInfo.customerLocale ?? settings.defaultLanguage);
  const t = getStrings(language);

  // Outstanding amount: prefer totalOutstanding (set by Shopify for unpaid),
  // fall back to totalPrice when zero.
  const amount = orderInfo.outstandingAmount > 0 ? orderInfo.outstandingAmount : orderInfo.totalAmount;
  const remittance = orderInfo.orderName || orderGid.split("/").pop() || "";

  const giroCodeUrl = (() => {
    const exp = Math.floor(Date.now() / 1000) + 60 * 60; // 1 hour
    const reqUrl = new URL(request.url);
    // Behind a reverse proxy that terminates TLS the inbound URL is http.
    // Trust X-Forwarded-Proto, otherwise force https for any non-localhost host.
    const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
    const isLocal = reqUrl.hostname === "localhost" || reqUrl.hostname === "127.0.0.1";
    const proto = forwardedProto ?? (isLocal ? reqUrl.protocol.replace(":", "") : "https");
    const origin = `${proto}://${reqUrl.host}`;
    const qs = signGiroCodeUrl({ shop, orderId: numericId, exp });
    return `${origin}/api/public/girocode.png?${qs}`;
  })();

  const dueDate = settings.paymentTermDays > 0
    ? addDays(new Date(), settings.paymentTermDays)
    : null;

  return cors(
    Response.json({
      showPaymentInstructions: true,
      payload: {
        language,
        heading: t.giroCodeCaption,
        giroCodeUrl,
        recipient: [settings.companyName, settings.legalForm].filter(Boolean).join(" "),
        bankName: settings.bankName,
        iban: settings.iban,
        bic: settings.bic,
        amountFormatted: formatMoney(amount, orderInfo.currency, language),
        reference: remittance,
        dueDateFormatted: dueDate ? formatDate(dueDate, language) : null,
        instructions: dueDate
          ? t.paymentTerms(settings.paymentTermDays, formatDate(dueDate, language))
          : t.paymentTermsImmediate,
        labels: {
          recipient: t.recipientLabel,
          bank: t.bankLabel,
          iban: t.ibanLabel,
          bic: t.bicLabel,
          amount: t.amountLabel,
          reference: t.referenceLabel,
        },
      },
    }),
  );
};

interface OrderInfo {
  isManual: boolean;
  totalAmount: number;
  outstandingAmount: number;
  currency: string;
  orderName: string;
  customerLocale?: string;
  txCount: number;
  manualFlags: Array<{ status?: string; manual?: boolean }>;
}

async function fetchOrderInfo(
  admin: { graphql: (q: string, opts?: { variables?: Record<string, unknown> }) => Promise<Response> },
  orderGid: string,
): Promise<OrderInfo | null> {
  const res = await admin.graphql(
    `#graphql
    query OrderPaymentInfo($id: ID!) {
      order(id: $id) {
        name
        currencyCode
        customerLocale
        totalPriceSet { shopMoney { amount } }
        totalOutstandingSet { shopMoney { amount } }
        transactions(first: 20) {
          status
          manualPaymentGateway
        }
      }
    }`,
    { variables: { id: orderGid } },
  );
  const json = (await res.json()) as {
    data?: {
      order?: {
        name?: string;
        currencyCode?: string;
        customerLocale?: string | null;
        totalPriceSet?: { shopMoney: { amount: string } };
        totalOutstandingSet?: { shopMoney: { amount: string } };
        transactions?: Array<{ status?: string; manualPaymentGateway?: boolean }>;
      } | null;
    };
  };
  const o = json.data?.order;
  if (!o) return null;
  const txs = o.transactions ?? [];
  const isManual = txs.some(
    (t) => t.manualPaymentGateway === true && t.status !== "FAILURE" && t.status !== "ERROR",
  );
  return {
    isManual,
    totalAmount: parseFloat(o.totalPriceSet?.shopMoney.amount ?? "0"),
    outstandingAmount: parseFloat(o.totalOutstandingSet?.shopMoney.amount ?? "0"),
    currency: o.currencyCode ?? "EUR",
    orderName: o.name ?? "",
    customerLocale: o.customerLocale ?? undefined,
    txCount: txs.length,
    manualFlags: txs.map((t) => ({ status: t.status, manual: t.manualPaymentGateway })),
  };
}
