"use client";

// ---------------------------------------------------------------------------
// KratosFlowForm — generic renderer for an Ory Kratos self-service flow
// ---------------------------------------------------------------------------
// Renders `flow.ui.nodes` into a form, submits to `flow.ui.action`, and shows
// the validation messages Kratos attaches to nodes / the flow.
//
// Node groups handled:
//   - default / password / code / profile  -> rendered as inputs & the submit
//   - oidc                                 -> rendered as social sign-in buttons
//   - hidden inputs (csrf_token, etc.)      -> rendered as <input type="hidden">
// ---------------------------------------------------------------------------

import { useMemo, useState } from "react";
import { Loader2 } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/forms/password-input";
import { submitFlow, type KratosFlow, type KratosUiNode } from "@/lib/kratos";

// ---------------------------------------------------------------------------
// Node classification helpers
// ---------------------------------------------------------------------------

function isHidden(node: KratosUiNode): boolean {
  return node.attributes.node_type === "input" && node.attributes.type === "hidden";
}

function isSubmit(node: KratosUiNode): boolean {
  return (
    node.attributes.node_type === "input" &&
    (node.attributes.type === "submit" || node.attributes.type === "button")
  );
}

function isOidc(node: KratosUiNode): boolean {
  return node.group === "oidc";
}

/** Human label for a node, falling back to the field name. */
function nodeLabel(node: KratosUiNode): string {
  return (
    node.meta?.label?.text ||
    node.attributes.label?.text ||
    node.attributes.name ||
    ""
  );
}

/** Pretty provider name for an oidc button ("apple" -> "Apple"). */
function providerName(node: KratosUiNode): string {
  const raw =
    String(node.attributes.value ?? "") ||
    nodeLabel(node).replace(/^sign in with /i, "");
  return raw.charAt(0).toUpperCase() + raw.slice(1);
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

interface KratosFlowFormProps {
  flow: KratosFlow;
  /**
   * Called after a submit. Receives the HTTP status, the parsed body, and
   * whether the request was a 2xx. The page decides what to do next
   * (redirect, advance a step, re-render the returned flow, ...).
   */
  onResult: (result: { status: number; ok: boolean; data: unknown }) => void;
  /** Optional override for the primary submit button label. */
  submitLabel?: string;
  /** Field names to omit from rendering (e.g. already-known email). */
  hideFields?: string[];
}

export function KratosFlowForm({
  flow,
  onResult,
  submitLabel,
  hideFields = [],
}: KratosFlowFormProps) {
  const [submitting, setSubmitting] = useState(false);
  const [activeSubmit, setActiveSubmit] = useState<string | null>(null);

  const { hiddenNodes, oidcNodes, fieldNodes, submitNodes } = useMemo(() => {
    const hidden: KratosUiNode[] = [];
    const oidc: KratosUiNode[] = [];
    const fields: KratosUiNode[] = [];
    const submits: KratosUiNode[] = [];
    for (const node of flow.ui.nodes) {
      if (isHidden(node)) hidden.push(node);
      else if (isOidc(node)) oidc.push(node);
      else if (isSubmit(node)) submits.push(node);
      else if (node.attributes.node_type === "input") fields.push(node);
    }
    return {
      hiddenNodes: hidden,
      oidcNodes: oidc,
      fieldNodes: fields.filter(
        (n) => !hideFields.includes(n.attributes.name ?? ""),
      ),
      submitNodes: submits,
    };
  }, [flow, hideFields]);

  /** The primary (non-oidc) submit button, if any. */
  const primarySubmit = submitNodes.find((n) => n.group !== "oidc");

  async function runSubmit(
    formEl: HTMLFormElement,
    submitName?: string,
    submitValue?: string,
  ) {
    if (submitting) return;
    setSubmitting(true);
    setActiveSubmit(submitValue ?? submitName ?? null);

    // Collect every input value from the form.
    const body: Record<string, string> = {};
    const data = new FormData(formEl);
    for (const [key, value] of data.entries()) {
      if (typeof value === "string") body[key] = value;
    }
    // The clicked submit button's name/value must be included so Kratos knows
    // which method was used (e.g. method=password, or provider=google).
    if (submitName && submitValue !== undefined) {
      body[submitName] = submitValue;
    }

    try {
      const result = await submitFlow(flow.ui.action, flow.ui.method, body);
      onResult(result);
    } finally {
      setSubmitting(false);
      setActiveSubmit(null);
    }
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = e.currentTarget;
    if (primarySubmit?.attributes.name) {
      runSubmit(
        form,
        primarySubmit.attributes.name,
        String(primarySubmit.attributes.value ?? ""),
      );
    } else {
      runSubmit(form);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      {/* Hidden inputs (csrf_token, flow id, method markers, ...) */}
      {hiddenNodes.map((node, i) => (
        <input
          key={`${node.attributes.name}-${i}`}
          type="hidden"
          name={node.attributes.name}
          defaultValue={String(node.attributes.value ?? "")}
        />
      ))}

      {/* Visible input fields */}
      {fieldNodes.map((node) => (
        <FieldNode key={node.attributes.name} node={node} />
      ))}

      {/* Primary submit */}
      {primarySubmit && (
        <Button type="submit" className="w-full" disabled={submitting}>
          {submitting && activeSubmit === String(primarySubmit.attributes.value ?? "") && (
            <Loader2 className="animate-spin" />
          )}
          {submitLabel ||
            nodeLabel(primarySubmit) ||
            "Continue"}
        </Button>
      )}

      {/* Social sign-in (oidc) */}
      {oidcNodes.length > 0 && (
        <div className="flex flex-col gap-2">
          {(fieldNodes.length > 0 || primarySubmit) && (
            <div className="relative my-1 text-center">
              <span className="relative z-10 bg-card px-2 text-xs text-muted-foreground">
                or continue with
              </span>
              <span className="absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-border" />
            </div>
          )}
          {oidcNodes.map((node) => (
            <Button
              key={String(node.attributes.value)}
              type="submit"
              variant="outline"
              className="w-full"
              disabled={submitting}
              formNoValidate
              onClick={(e) => {
                e.preventDefault();
                runSubmit(
                  e.currentTarget.form as HTMLFormElement,
                  node.attributes.name,
                  String(node.attributes.value ?? ""),
                );
              }}
            >
              {submitting && activeSubmit === String(node.attributes.value ?? "") && (
                <Loader2 className="animate-spin" />
              )}
              {nodeLabel(node) || `Continue with ${providerName(node)}`}
            </Button>
          ))}
        </div>
      )}
    </form>
  );
}

// ---------------------------------------------------------------------------
// FieldNode — a single visible input + its label + node-level messages
// ---------------------------------------------------------------------------

function FieldNode({ node }: { node: KratosUiNode }) {
  const name = node.attributes.name ?? "";
  const type = node.attributes.type ?? "text";
  const label = nodeLabel(node);
  const isPassword = type === "password";
  const errors = node.messages?.filter((m) => m.type === "error") ?? [];
  const hasError = errors.length > 0;
  const fieldId = `kratos-field-${name}`;

  return (
    <div className="flex flex-col gap-2">
      {label && <Label htmlFor={fieldId}>{label}</Label>}
      {isPassword ? (
        <PasswordInput
          id={fieldId}
          name={name}
          required={node.attributes.required}
          disabled={node.attributes.disabled}
          autoComplete={node.attributes.autocomplete ?? "current-password"}
          aria-invalid={hasError}
          aria-describedby={hasError ? `${fieldId}-error` : undefined}
        />
      ) : (
        <Input
          id={fieldId}
          name={name}
          type={type}
          required={node.attributes.required}
          disabled={node.attributes.disabled}
          autoComplete={node.attributes.autocomplete}
          defaultValue={String(node.attributes.value ?? "")}
          aria-invalid={hasError}
          aria-describedby={hasError ? `${fieldId}-error` : undefined}
        />
      )}
      {errors.map((m, i) => (
        <p
          key={m.id ?? i}
          id={`${fieldId}-error`}
          role="alert"
          className="text-sm text-destructive"
        >
          {m.text}
        </p>
      ))}
    </div>
  );
}
