"use client";

// components/flow/PurchaseFlow.tsx
// The /participar multi-step client orchestrator (agent B). Drives the honest
// impulse micro-purchase flow (target: under a minute):
//
//   register -> upsell -> review (Yape phone + codigo) -> /participar/gracias
//
// State-of-truth discipline: the client NEVER computes the authoritative price.
// It proposes a tier + flags; POST /api/orders recomputes chances + amount and
// returns the figure shown on the pay button. Niubiz's synchronous authorization
// (POST /api/orders/[id]/confirm) is the only source of truth for a confirmed
// payment; a decline lets the buyer retry with a fresh codigo without losing
// their order.
//
// Back-navigation preserves entered data so the user never retypes name +
// contact (a top mobile-abandonment cause).

import * as React from "react";
import { useRouter } from "next/navigation";
import type { CreateOrderResponse } from "@/lib/types";
import {
  confirmDemoPayment,
  confirmNiubizPayment,
  createOrder,
  FlowApiError,
  registerParticipant,
} from "./api";
import { FlowProgress } from "./FlowProgress";
import { RegisterStep } from "./RegisterStep";
import { UpsellStep } from "./UpsellStep";
import { ReviewStep } from "./ReviewStep";
import { OpenInBrowserHint } from "./OpenInBrowserHint";
import {
  type FlowAttribution,
  type FlowRaffle,
  type FlowState,
  type FlowStep,
  type PublicPricingTier,
  type RegisterDraft,
} from "./types";

export interface PurchaseFlowProps {
  raffle: FlowRaffle;
  tiers: PublicPricingTier[];
  attribution: FlowAttribution;
}

const EMPTY_DRAFT: RegisterDraft = {
  fullName: "",
  phone: "",
  email: "",
  dataConsent: false,
  termsAccepted: false,
  marketingConsent: false,
};

// The Turnstile-failure copy tells the buyer to reload the page, so a reload
// must not wipe the name/phone/email they already typed (retyping on mobile is
// a top abandonment cause). We persist ONLY those three contact fields to
// sessionStorage. The consent booleans (dataConsent/termsAccepted/
// marketingConsent) are deliberately NOT persisted: consent must be a fresh,
// express, affirmative action on every load per Ley 29733 (legal.md 2), so they
// always reset to false on reload.
const DRAFT_STORAGE_KEY = "teleton:flow-draft";

function readDraftFromStorage(): RegisterDraft {
  if (typeof window === "undefined") return EMPTY_DRAFT;
  try {
    const raw = window.sessionStorage.getItem(DRAFT_STORAGE_KEY);
    if (!raw) return EMPTY_DRAFT;
    const parsed = JSON.parse(raw) as Partial<RegisterDraft>;
    return {
      ...EMPTY_DRAFT,
      fullName:
        typeof parsed.fullName === "string" ? parsed.fullName : "",
      phone: typeof parsed.phone === "string" ? parsed.phone : "",
      email: typeof parsed.email === "string" ? parsed.email : "",
      // Consent flags are never restored: see DRAFT_STORAGE_KEY note above.
    };
  } catch {
    return EMPTY_DRAFT;
  }
}

export function PurchaseFlow({
  raffle,
  tiers,
  attribution,
}: PurchaseFlowProps) {
  const router = useRouter();

  const [state, setState] = React.useState<FlowState>({
    step: "register",
    draft: EMPTY_DRAFT,
    registration: null,
    selectedTierId: null, // nothing pre-selected beyond the base 1 chance
    feeOptIn: false, // fee toggle OFF by default
    order: null,
  });
  const [submitting, setSubmitting] = React.useState(false);
  const [paying, setPaying] = React.useState(false);
  const [registerError, setRegisterError] = React.useState<string | null>(null);
  const [reviewError, setReviewError] = React.useState<string | null>(null);
  // Mandatory Niubiz denied-transaction receipt fields (numero de pedido +
  // fecha y hora), shown alongside reviewError's descripcion de la denegacion.
  const [reviewErrorMeta, setReviewErrorMeta] = React.useState<{
    purchaseNumber: string;
    transactionDate: string;
  } | null>(null);
  // The buyer proves payment with their Yape phone + a single-use 6-digit
  // codigo de aprobacion, confirmed synchronously server-side by Niubiz.
  const [yapePhone, setYapePhone] = React.useState("");
  const [yapeOtp, setYapeOtp] = React.useState("");
  // Flips to true once the mount-time storage hydration has run. Used to remount
  // RegisterStep (via key) so its internal field state re-reads the restored
  // `initial`, since RegisterStep seeds that state only on mount.
  const [hydratedDraft, setHydratedDraft] = React.useState(false);

  // ── Register-draft persistence (survives a reload; see DRAFT_STORAGE_KEY) ──
  // Read sessionStorage ONCE on mount and seed only the contact fields. We do
  // NOT read storage during useState init: this component is server-rendered,
  // so a lazy init would diverge from the server's empty first paint and trip a
  // hydration mismatch. Reading in a mount-only effect keeps the first client
  // paint identical to the server, then fills in the saved values. This is the
  // one sanctioned setState-in-effect here (a single hydration seed, not a
  // cascade), so the project rule is disabled for this line with that rationale.
  React.useEffect(() => {
    const saved = readDraftFromStorage();
    if (!saved.fullName && !saved.phone && !saved.email) return;
    // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time storage hydration; consent flags intentionally excluded (Ley 29733)
    setState((s) => ({
      ...s,
      draft: {
        ...s.draft,
        fullName: saved.fullName,
        phone: saved.phone,
        email: saved.email,
      },
    }));
    setHydratedDraft(true);
  }, []);

  // Write the contact fields back whenever the draft changes (back-navigation
  // and submit both flow through state.draft). Consent flags are never written.
  React.useEffect(() => {
    if (typeof window === "undefined") return;
    // Do not let the empty first-paint draft overwrite stored values before the
    // hydration seed has had a chance to restore them. Once anything is typed
    // (or hydration restored values), normal writes resume.
    if (
      !hydratedDraft &&
      !state.draft.fullName &&
      !state.draft.phone &&
      !state.draft.email
    ) {
      return;
    }
    try {
      window.sessionStorage.setItem(
        DRAFT_STORAGE_KEY,
        JSON.stringify({
          fullName: state.draft.fullName,
          phone: state.draft.phone,
          email: state.draft.email,
        }),
      );
    } catch {
      // Private-mode or quota errors are non-fatal: persistence is a nicety,
      // not a requirement for the flow to work.
    }
  }, [
    hydratedDraft,
    state.draft.fullName,
    state.draft.phone,
    state.draft.email,
  ]);

  const selectedTier =
    tiers.find((t) => t.id === state.selectedTierId) ?? null;

  function goTo(step: FlowStep) {
    setState((s) => ({ ...s, step }));
    // Move focus to the top of the new step for screen-reader + keyboard users.
    if (typeof window !== "undefined") {
      window.scrollTo({ top: 0, behavior: "auto" });
    }
  }

  // ── Step 1: register ──────────────────────────────────────
  async function handleRegister(draft: RegisterDraft, turnstileToken: string) {
    setRegisterError(null);
    setSubmitting(true);
    // Persist the draft immediately so back-navigation keeps the input.
    setState((s) => ({ ...s, draft }));
    try {
      const res = await registerParticipant({
        raffleSlug: raffle.slug,
        fullName: draft.fullName.trim(),
        phone: draft.phone.trim(),
        email: draft.email.trim(),
        dataConsent: draft.dataConsent,
        marketingConsent: draft.marketingConsent,
        termsVersion: raffle.termsVersion,
        turnstileToken,
      });
      setState((s) => ({ ...s, registration: res, step: "upsell" }));
      // Pre-fill the Yape phone from the just-validated registration phone so the
      // buyer only enters the codigo de aprobacion on the pay step (editable if they
      // pay from a different Yape). Removes half the typing on the highest-friction
      // step. MIRRORS validation.ts isLikelyPeruPhone: strip separators THEN the optional
      // +51/51 country prefix BEFORE slicing. A naive first-9-digits slice would turn
      // "+51 912345678" into "519123456" (country code kept, last 2 real digits
      // dropped) — a wrong Yape number that passes every length check and is a
      // guaranteed decline at Niubiz.
      setYapePhone(
        draft.phone
          .replace(/[\s()-]/g, "")
          .replace(/^\+?51/, "")
          .replace(/\D/g, "")
          .slice(0, 9),
      );
      window.scrollTo({ top: 0, behavior: "auto" });
    } catch (err) {
      setRegisterError(
        err instanceof FlowApiError
          ? err.message
          : "No pudimos completar tu inscripción. Intenta de nuevo.",
      );
    } finally {
      setSubmitting(false);
    }
  }

  // ── Step 3 -> create order, then confirm with Niubiz ──────
  async function handlePay() {
    if (!state.registration) {
      setReviewError("Vuelve a inscribirte para continuar.");
      return;
    }
    // Validate the phone + codigo de aprobacion BEFORE creating an order, so a
    // typo never spends an order-creation attempt.
    if (yapePhone.trim().length !== 9 || yapeOtp.trim().length !== 6) {
      setReviewError(
        "Ingresa tu número de Yape (9 dígitos) y tu código de aprobación (6 dígitos).",
      );
      return;
    }
    setReviewError(null);
    setReviewErrorMeta(null);
    setPaying(true);
    try {
      // Always create a fresh order on pay; the server recomputes the authoritative
      // amount + chances from the CURRENT selection. A Yape retry (the single-use
      // codigo de aprobacion rotates ~2 min) thus gets a brand-new, never-stale,
      // never-expired pending order with its own unique purchaseNumber, which
      // sidesteps both the stale-tier-amount and expired-reuse hazards. The
      // returned amount is the only authoritative total.
      const order: CreateOrderResponse = await createOrder({
        raffleId: state.registration.raffleId,
        participantId: state.registration.participantId,
        tierId: state.selectedTierId,
        feeOptIn: state.feeOptIn,
        method: "yape",
        utmSource: attribution.utmSource,
        utmCampaign: attribution.utmCampaign,
        influencerCode: attribution.influencerCode,
      });
      setState((s) => ({ ...s, order }));

      // DEMO MODE: skip Niubiz entirely. Confirm the entry server-side
      // (POST /api/demo/confirm, itself hard-gated on DEMO_MODE) and route to
      // the confirmation screen, which polls status and flips to the success
      // state. The pay button keeps its real "Pagar S/X" label; the site-wide
      // MODO DEMO banner is the honest disclosure. A short beat makes the
      // handoff read like a real payment.
      if (process.env.NEXT_PUBLIC_DEMO_MODE === "true") {
        await new Promise((resolve) => setTimeout(resolve, 900));
        await confirmDemoPayment(order.purchaseId);
        router.push(`/participar/gracias?purchase=${order.purchaseId}`);
        return;
      }

      // NIUBIZ: confirm synchronously with the buyer's phone + codigo de aprobacion.
      // The server authorizes with the server-locked amount and, on approval, mints
      // the entry; a DECLINE returns ok:false so the buyer can retry with a fresh
      // code without losing their order.
      const result = await confirmNiubizPayment(order.purchaseId, {
        phoneNumber: yapePhone.trim(),
        otp: yapeOtp.trim(),
      });
      if (result.ok) {
        router.push(`/participar/gracias?purchase=${order.purchaseId}`);
        return;
      }
      setReviewError(
        result.message ??
          "Tu pago con Yape no fue aprobado. Genera un código nuevo e intenta otra vez.",
      );
      // Mandatory denied-transaction receipt fields (Niubiz certification):
      // numero de pedido + fecha y hora, shown alongside the decline message.
      if (result.purchaseNumber && result.transactionDate) {
        setReviewErrorMeta({
          purchaseNumber: result.purchaseNumber,
          transactionDate: result.transactionDate,
        });
      }
      setYapeOtp("");
      setPaying(false);
    } catch (err) {
      setReviewError(
        err instanceof Error
          ? err.message
          : "No pudimos iniciar el pago. Intenta de nuevo.",
      );
      setPaying(false);
    }
  }

  return (
    <div className="mx-auto flex w-full max-w-[480px] flex-col gap-lg px-5 py-8">
      {/* Escape hatch for Instagram / TikTok in-app webviews where the hosted
          popup or cookie flow can break. Surfaced ONCE at the top, on flow
          entry, so the buyer can move to a real browser before they ever reach
          the pay step (a broken payment popup is a conversion blocker). It
          renders nothing on a normal browser and is dismissible. */}
      <OpenInBrowserHint />

      <FlowProgress current={state.step} />

      {state.step === "register" ? (
        // `key` flips once after the mount-time storage hydration so RegisterStep
        // remounts and re-seeds its internal field state from the restored
        // `initial`. RegisterStep reads `initial` only on mount, so without this
        // the restored name/phone/email would not reach the inputs after reload.
        <RegisterStep
          key={hydratedDraft ? "hydrated" : "initial"}
          raffle={raffle}
          initial={state.draft}
          submitting={submitting}
          serverError={registerError}
          onSubmit={handleRegister}
        />
      ) : null}

      {state.step === "upsell" ? (
        <UpsellStep
          raffle={raffle}
          tiers={tiers}
          selectedTierId={state.selectedTierId}
          feeOptIn={state.feeOptIn}
          onSelect={(tierId) =>
            setState((s) => ({ ...s, selectedTierId: tierId }))
          }
          onBack={() => goTo("register")}
          onContinue={() => goTo("review")}
        />
      ) : null}

      {state.step === "review" ? (
        <ReviewStep
          raffle={raffle}
          draft={state.draft}
          selectedTier={selectedTier}
          feeOptIn={state.feeOptIn}
          onToggleFee={(next) =>
            setState((s) => ({ ...s, feeOptIn: next, order: null }))
          }
          serverAmountCents={state.order?.amountCents ?? null}
          serverChances={state.order?.chances ?? null}
          paying={paying}
          serverError={reviewError}
          serverErrorMeta={reviewErrorMeta}
          onBack={() => goTo("upsell")}
          onPay={handlePay}
          yapePhone={yapePhone}
          yapeOtp={yapeOtp}
          onYapePhoneChange={setYapePhone}
          onYapeOtpChange={setYapeOtp}
        />
      ) : null}

      {/* Quiet, persistent help affordance. Present on every step so a stuck
          buyer always has a way out. 44px min target + standard focus ring. */}
      <p className="font-body text-caption text-muted-label">
        <a
          href="mailto:teleton@teleton.pe"
          className="inline-flex min-h-[44px] items-center text-steelblue underline focus-visible:outline focus-visible:outline-[3px] focus-visible:outline-offset-2 focus-visible:outline-steelblue"
        >
          ¿Necesitas ayuda?
        </a>
      </p>
    </div>
  );
}
