"use client";

/**
 * Integrations → M-Pesa (PRD 10.1; plan F: IG-01 – IG-03, IG-06, ST-18), on the design's integration page: status and
 * environment (`IntegrationHeader`, switching to Live confirms with a reason), each branch's codes and write-only keys
 * (`MpesaBranchKeys`, encrypted by the API and never returned), a connection test, C2B registration, payments made
 * straight to a paybill or till waiting to be matched (`C2BRow`), and the event log (`IntegrationLog`).
 */

import * as React from "react";
import { RefreshCwIcon } from "lucide-react";

import { C2BRow, type C2BPayment } from "@/components/medora/admin-more";
import { ConfirmDialog } from "@/components/medora/confirm-dialog";
import { PanelError } from "@/components/medora/errors";
import { MedoraPageHeader } from "@/components/medora/page-header";
import { IntegrationHeader, IntegrationLog, ProviderChecks, type ProviderCheck } from "@/components/medora/settings-ops";
import { MpesaBranchKeys, type MpesaBranch } from "@/components/medora/setup-more";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/toast";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { CustomersApi, MpesaApi, type C2bPaymentRow, type MpesaEventRow, type MpesaOverview } from "@/lib/api";
import { useTabParam } from "@/lib/use-tab-param";
import { formatPhone, formatKES } from "@/lib/format";
import { useMenuPermissions } from "@/lib/permissions";
import { Loading } from "@/components/medora/loading";

const PAGE = "Integrations/M-Pesa";

const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(" ") : m || fallback;
};
const when = (v: string | null | undefined) => (v ? new Date(v).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) : "");

type Match = { row: C2bPaymentRow; kind: "sale" | "customer" | "notOurs"; receiptId: string; customerId: string; reason: string };

export default function MpesaIntegrationPage() {
  const { can, loaded } = useMenuPermissions();
  const may = (a: string) => loaded && can(PAGE, a);
  const [viewParam, setView] = useTabParam(); // page tabs change the URL (tabs spec 7.1)
  const view = viewParam === "payments" || viewParam === "log" ? viewParam : "setup";
  const [data, setData] = React.useState<MpesaOverview | null>(null);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [branches, setBranches] = React.useState<MpesaBranch[]>([]);
  const [events, setEvents] = React.useState<MpesaEventRow[] | null>(null);
  const [payments, setPayments] = React.useState<C2bPaymentRow[] | null>(null);
  const [allPayments, setAllPayments] = React.useState(false);
  const [goingLive, setGoingLive] = React.useState(false);
  const [busy, setBusy] = React.useState("");
  const [match, setMatch] = React.useState<Match | null>(null);
  const [customers, setCustomers] = React.useState<{ id: number; name: string; phone: string }[]>([]);

  const show = React.useCallback((o: MpesaOverview) => {
    setData(o);
    setBranches(
      o.branches.map((b) => ({
        branch: b.branch,
        type: b.till ? "Till (Buy Goods)" : "Paybill",
        shortcode: b.shortcode,
        till: b.till,
        keys: { set: b.keys.set && b.keys.readable, lastChanged: b.keys.at ? { at: when(b.keys.at), by: b.keys.by ?? "someone" } : undefined },
        passkey: { set: b.passkey.set && b.passkey.readable, lastChanged: b.passkey.at ? { at: when(b.passkey.at), by: b.passkey.by ?? "someone" } : undefined },
        lastTest: b.lastTest ? { ok: b.lastTest.ok, at: when(b.lastTest.at), note: b.lastTest.note ?? undefined } : undefined,
      })),
    );
  }, []);

  const load = React.useCallback(async () => {
    try {
      show(await MpesaApi.overview());
      setFailed(false);
    } catch (e) {
      setFailed(true);
      toast.add({ type: "error", title: "Couldn't load M-Pesa", description: apiError(e, "Try again.") });
    }
  }, [show]);
  const loadEvents = React.useCallback(() => {
    MpesaApi.log(200)
      .then(setEvents)
      .catch(() => setEvents([]));
  }, []);
  const loadPayments = React.useCallback(() => {
    setPayments(null);
    MpesaApi.payments(allPayments ? "all" : "unmatched")
      .then(setPayments)
      .catch(() => setPayments([]));
  }, [allPayments]);

  React.useEffect(() => {
    void load();
  }, [load]);
  React.useEffect(() => {
    if (view === "log") loadEvents();
    if (view === "payments") loadPayments();
  }, [view, loadEvents, loadPayments]);
  React.useEffect(() => {
    if (!match || match.kind !== "customer" || customers.length) return;
    CustomersApi.getAll()
      .then((cs: { id: number; name: string; phone?: string | null }[]) => setCustomers(cs.map((c) => ({ id: c.id, name: c.name, phone: c.phone ?? "" })).sort((a, b) => a.name.localeCompare(b.name))))
      .catch(() => setCustomers([]));
  }, [match, customers.length]);

  const idOf = (name: string) => data?.branches.find((b) => b.branch === name)?.branchId;

  const setEnv = async (environment: "sandbox" | "live", reason?: string) => {
    try {
      show(await MpesaApi.setEnvironment(environment, reason));
      toast.add({ title: environment === "live" ? "M-Pesa is Live" : "M-Pesa switched to Sandbox", description: "Test each branch's connection now." });
    } catch (e) {
      toast.add({ type: "error", title: "Not switched", description: apiError(e, "Try again.") });
      throw e;
    }
  };

  const test = async (name: string) => {
    const id = idOf(name);
    if (!id) return;
    setBusy(`test-${id}`);
    try {
      const r = await MpesaApi.test(id);
      toast.add({ type: r.ok ? undefined : "error", title: r.ok ? `${name} connected` : `${name} test failed`, description: r.detail });
      await load();
    } catch (e) {
      toast.add({ type: "error", title: "Test not run", description: apiError(e, "Try again.") });
    } finally {
      setBusy("");
    }
  };

  const register = async (branchId: number, name: string) => {
    setBusy(`c2b-${branchId}`);
    try {
      const r = await MpesaApi.registerC2b(branchId);
      toast.add({ type: r.ok ? undefined : "error", title: r.ok ? `${name}: paybill and till payments now come here` : `${name} not registered`, description: r.detail });
      await load();
    } catch (e) {
      toast.add({ type: "error", title: "Not registered", description: apiError(e, "Try again.") });
    } finally {
      setBusy("");
    }
  };

  const doMatch = async () => {
    if (!match) return;
    setBusy("match");
    try {
      await MpesaApi.match(
        match.row.id,
        match.kind === "notOurs" ? { notOurs: true, reason: match.reason.trim() } : match.kind === "sale" ? { kind: "sale", receiptId: match.receiptId.trim() } : { kind: "customer", customerId: Number(match.customerId) },
      );
      toast.add({ title: match.kind === "notOurs" ? `${match.row.transId} marked not ours` : `${match.row.transId} matched`, description: match.kind === "notOurs" ? undefined : "It's on the customer's account and posted to the branch's M-Pesa account." });
      setMatch(null);
      loadPayments();
      void load();
    } catch (e) {
      toast.add({ type: "error", title: "Not matched", description: apiError(e, "Try again.") });
    } finally {
      setBusy("");
    }
  };

  if (failed && !data)
    return (
      <div className="medora mx-auto w-full max-w-6xl py-6">
        <PanelError framed what="M-Pesa" onRetry={() => void load()} />
      </div>
    );
  if (!data || !loaded)
    return (
      <Loading what="M-Pesa" shape="list" rows={3} />
    );

  const env = data.environment === "live" ? "Live" : "Sandbox";
  const unreadable = data.branches.filter((b) => !b.keys.readable || !b.passkey.readable).map((b) => b.branch);
  const checks: ProviderCheck[] = [
    data.encryption.keySet
      ? { label: "Keys encrypted", ok: data.encryption.plain === 0, detail: data.encryption.plain ? `${data.encryption.plain} key(s) still stored unencrypted; they're encrypted when the API next starts.` : "Stored encrypted and never shown again after saving." }
      : { label: "Keys not encrypted", ok: false, detail: "The server has no SETTINGS_ENCRYPTION_KEY, so keys are stored as typed. Ask your administrator to set it and restart the API." },
    ...(unreadable.length ? [{ label: "Keys to enter again", ok: false, detail: `${unreadable.join(", ")}: saved under a different server key, so they can't be read. Enter them again.` }] : []),
    { label: "Payment results address", ok: data.callbackFromServer, detail: data.callbackFromServer ? `Safaricom sends results to ${data.callbackUrl}` : `The server has no MPESA_CALLBACK_URL, so results go to ${data.callbackUrl}. Ask your administrator to set it.` },
    { label: "Paybill and till payments (C2B)", ok: !data.c2b.problem, detail: data.c2b.problem ?? `Confirmed to ${data.c2b.base}${data.c2b.ipCheck ? ", from Safaricom's addresses only" : " (on the sandbox any sender is accepted)"}.` },
  ];

  return (
    <div className="medora mx-auto w-full max-w-6xl space-y-4">
      <MedoraPageHeader title="M-Pesa" description="Each branch's M-Pesa keys, the sandbox or live switch, and paybill payments." />

      <div className="flex flex-wrap items-center justify-between gap-3">
        <IntegrationHeader name="M-Pesa (Daraja)" status={data.status} environment={env} lastOk={data.lastOk ? when(data.lastOk) : undefined} />
        {may("Update") ? (
          <ToggleGroup variant="outline" size="sm" spacing={0} aria-label="Environment" value={[env]} onValueChange={(v: string[]) => v[0] && v[0] !== env && (v[0] === "Live" ? setGoingLive(true) : void setEnv("sandbox", "Switched back to the sandbox").catch(() => {}))}>
            <ToggleGroupItem value="Sandbox">Sandbox</ToggleGroupItem>
            <ToggleGroupItem value="Live">Live</ToggleGroupItem>
          </ToggleGroup>
        ) : null}
      </div>

      <Tabs value={view} onValueChange={(v) => setView(v as typeof view)}>
        <TabsList variant="line" className="w-full justify-start border-b border-border">
          <TabsTrigger value="setup" className="flex-none">
            Branches and keys
          </TabsTrigger>
          <TabsTrigger value="payments" className="flex-none">
            Payments to match{data.unmatched ? ` (${data.unmatched})` : ""}
          </TabsTrigger>
          <TabsTrigger value="log" className="flex-none">
            Event log
          </TabsTrigger>
        </TabsList>
      </Tabs>

      {view === "setup" ? (
        <div className="space-y-4">
          <ProviderChecks checks={checks} />
          {branches.length ? (
            <MpesaBranchKeys
              branches={branches}
              onChange={setBranches}
              environment={env}
              callbackUrl={data.callbackUrl}
              onTest={(name) => void test(name)}
              onSave={
                may("Update")
                  ? async (name, secrets) => {
                      const id = idOf(name);
                      const row = branches.find((x) => x.branch === name);
                      if (!id || !row) return;
                      try {
                        show(await MpesaApi.saveBranch(id, { shortcode: row.shortcode, till: row.type === "Till (Buy Goods)" ? row.till : "", ...secrets }));
                        toast.add({ title: `${name} M-Pesa settings saved`, description: secrets.consumerKey || secrets.passkey ? "Keys are encrypted and won't be shown again. Test the connection now." : undefined });
                      } catch (e) {
                        toast.add({ type: "error", title: `${name} not saved`, description: apiError(e, "Try again.") });
                        throw e;
                      }
                    }
                  : undefined
              }
            />
          ) : (
            <p className="text-sm text-muted-foreground">Add a branch under Settings first.</p>
          )}
          {busy.startsWith("test-") ? (
            <p className="flex items-center gap-2 text-sm text-muted-foreground">
              <Spinner /> Testing the connection…
            </p>
          ) : null}

          <section aria-label="Paybill and till payments" className="space-y-2 rounded-lg border border-[var(--color-border-panel)] p-4 bg-card">
            <h2 className="text-sm font-semibold">Paybill and till payments (C2B)</h2>
            <p className="text-sm text-muted-foreground">
              Register a branch so payments customers make straight to its paybill or till come here. One whose account number is a credit sale&apos;s number, for exactly what the sale left unpaid, goes on that customer&apos;s account by itself; the rest wait under Payments to match, or pay a sale at the till by their M-Pesa code.
            </p>
            <ul className="divide-y divide-[var(--color-border-subtle)] text-sm">
              {data.branches.map((b) => (
                <li key={b.branchId} className="flex flex-wrap items-center justify-between gap-2 py-2">
                  <span>
                    <span className="font-medium">{b.branch}</span>
                    <span className="block text-xs text-muted-foreground">{b.c2bRegisteredAt ? `Registered ${when(b.c2bRegisteredAt)} (${env})` : "Not registered"}</span>
                  </span>
                  {may("Update") ? (
                    <Button size="sm" variant="outline" disabled={!!busy || !!data.c2b.problem || !b.shortcode || !b.keys.set} onClick={() => void register(b.branchId, b.branch)}>
                      {busy === `c2b-${b.branchId}` ? <Spinner /> : null}
                      {b.c2bRegisteredAt ? "Register again" : "Register"}
                    </Button>
                  ) : null}
                </li>
              ))}
            </ul>
          </section>
        </div>
      ) : null}

      {view === "payments" ? (
        <div className="space-y-3">
          <div className="flex flex-wrap items-center justify-between gap-2">
            <label className="inline-flex items-center gap-2 text-sm">
              <input type="checkbox" className="size-4" checked={allPayments} onChange={(e) => setAllPayments(e.target.checked)} />
              Show matched and not-ours payments too
            </label>
            <Button size="sm" variant="ghost" onClick={loadPayments}>
              <RefreshCwIcon data-icon="inline-start" />
              Refresh
            </Button>
          </div>
          {payments === null ? (
            <Loading what="payments" shape="list" rows={4} />
          ) : payments.length ? (
            <ul className="divide-y divide-[var(--color-border-subtle)] rounded-lg border border-[var(--color-border-panel)] bg-card">
              {payments.map((p) => {
                const row: C2BPayment = { code: p.transId, at: `${when(p.transTime ?? p.createdAt)}${p.branch ? `, ${p.branch}` : ""}`, amount: p.amount, phone: p.phone ?? "", payer: p.payer ?? "Unknown payer", account: p.billRef ?? "", suggestion: p.suggestion ?? undefined };
                const start = (kind: Match["kind"]) => setMatch({ row: p, kind, receiptId: p.suggestion?.kind === "sale" ? p.suggestion.ref : "", customerId: p.suggestion?.kind === "customer" ? p.suggestion.ref : "", reason: "" });
                return p.status === "Unmatched" && may("Update") ? (
                  <React.Fragment key={p.id}>
                    <C2BRow p={row} onMatch={() => start(p.suggestion?.kind ?? "customer")} onSkip={() => start("notOurs")} />
                    <li className="flex justify-end px-3 pb-2">
                      <Button size="xs" variant="ghost" onClick={() => start("customer")}>
                        Match by hand…
                      </Button>
                    </li>
                  </React.Fragment>
                ) : (
                  <li key={p.id} className="flex flex-wrap items-start justify-between gap-2 px-3 py-2 text-sm">
                    <span>
                      <span className="font-semibold tabular-nums">{formatKES(p.amount, { decimals: true })}</span> <span className="font-mono text-xs">{p.transId}</span>
                      <span className="block text-xs text-muted-foreground">
                        {row.at} · {row.payer} · account typed &ldquo;{p.billRef || "(blank)"}&rdquo;
                      </span>
                    </span>
                    <span className="text-right text-xs">
                      <span className="font-medium">{p.status === "Unmatched" ? "Waiting" : p.status}</span>
                      {p.matchedHow ? <span className="block text-muted-foreground">{p.matchedHow === "Till" ? "Used at the till" : p.matchedHow === "Auto" ? "Matched by itself" : "By hand"}</span> : null}
                      {p.note ? <span className="block text-muted-foreground">{p.note}</span> : null}
                    </span>
                  </li>
                );
              })}
            </ul>
          ) : (
            <p className="py-10 text-center text-sm text-muted-foreground">{allPayments ? "No paybill or till payments have come in yet." : "Nothing waiting to be matched."}</p>
          )}
        </div>
      ) : null}

      {view === "log" ? (
        events === null ? (
          <Loading what="the event log" columns={4} rows={8} />
        ) : events.length ? (
          <IntegrationLog events={events.map((e) => ({ id: String(e.id), at: when(e.createdAt), event: e.event, ref: e.ref ?? "", ok: e.ok, ignored: e.ignored, detail: e.detail ?? undefined }))} />
        ) : (
          <p className="py-10 text-center text-sm text-muted-foreground">Nothing logged yet. Prompts, results, tests and paybill payments appear here as they happen.</p>
        )
      ) : null}

      <ConfirmDialog
        open={goingLive}
        onOpenChange={setGoingLive}
        level="B"
        title="Switch M-Pesa to Live?"
        consequences={["Customers will be charged real money from the next payment.", "Each branch needs its live shortcode and keys from Safaricom first.", "Test each branch's connection straight after switching."]}
        reasons={["Going live after testing", "Switching back after a sandbox test", "Other"]}
        actionLabel="Switch to Live"
        onConfirm={async ({ reason, details }) => {
          await setEnv("live", [reason, details].filter(Boolean).join(": "));
          setGoingLive(false);
        }}
      />

      <Dialog open={!!match} onOpenChange={(o) => !o && busy !== "match" && setMatch(null)}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{match?.kind === "notOurs" ? `Not ours: ${match?.row.transId}` : `Match ${match?.row.transId}`}</DialogTitle>
            <DialogDescription>
              {match ? `${formatKES(match.row.amount, { decimals: true })} from ${match.row.payer ?? "an unknown payer"}, account typed “${match.row.billRef || "(blank)"}”.` : ""}
            </DialogDescription>
          </DialogHeader>
          {match ? (
            <div className="space-y-3">
              <Field>
                <FieldLabel htmlFor="mm-kind">What it pays</FieldLabel>
                <NativeSelect id="mm-kind" value={match.kind} onChange={(e) => setMatch({ ...match, kind: e.target.value as Match["kind"] })}>
                  <NativeSelectOption value="customer">A customer&apos;s account</NativeSelectOption>
                  <NativeSelectOption value="sale">A credit sale (its customer&apos;s account)</NativeSelectOption>
                  <NativeSelectOption value="notOurs">Not ours: refunded to the customer outside Medora</NativeSelectOption>
                </NativeSelect>
              </Field>
              {match.kind === "sale" ? (
                <Field>
                  <FieldLabel htmlFor="mm-sale">Sale number</FieldLabel>
                  <Input id="mm-sale" className="font-mono" value={match.receiptId} onChange={(e) => setMatch({ ...match, receiptId: e.target.value })} />
                </Field>
              ) : null}
              {match.kind === "customer" ? (
                <Field>
                  <FieldLabel htmlFor="mm-cust">Customer</FieldLabel>
                  <NativeSelect id="mm-cust" value={match.customerId} onChange={(e) => setMatch({ ...match, customerId: e.target.value })}>
                    <NativeSelectOption value="">Choose a customer</NativeSelectOption>
                    {customers.map((c) => (
                      <NativeSelectOption key={c.id} value={String(c.id)}>
                        {c.name}
                        {c.phone ? ` (${formatPhone(c.phone)})` : ""}
                      </NativeSelectOption>
                    ))}
                  </NativeSelect>
                </Field>
              ) : null}
              {match.kind === "notOurs" ? (
                <Field>
                  <FieldLabel htmlFor="mm-reason">Why</FieldLabel>
                  <Input id="mm-reason" placeholder="Refunded to the customer, paid the wrong till" value={match.reason} onChange={(e) => setMatch({ ...match, reason: e.target.value })} />
                </Field>
              ) : null}
            </div>
          ) : null}
          <DialogFooter>
            <Button variant="outline" disabled={busy === "match"} onClick={() => setMatch(null)}>
              Cancel
            </Button>
            <Button
              variant={match?.kind === "notOurs" ? "destructive" : "default"}
              disabled={busy === "match" || !match || (match.kind === "sale" && !match.receiptId.trim()) || (match.kind === "customer" && !match.customerId) || (match.kind === "notOurs" && match.reason.trim().length < 3)}
              onClick={() => void doMatch()}
            >
              {busy === "match" ? <Spinner /> : null}
              {match?.kind === "notOurs" ? "Mark not ours" : "Match payment"}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}
