"use client";

import QRCode from "qrcode";
import { useState, type FormEvent } from "react";
import { Download, Laptop, ShieldCheck } from "lucide-react";
import { Badge, Button, ConfirmDialog, Dialog, Field, Input, PageHeader, Panel, Skeleton } from "@/components/ui/kit";
import { useToast } from "@/components/ui/toast";
import { useSession } from "@/components/session";
import { CopyField } from "@/components/developers/common";
import { api, ApiError } from "@/lib/api";
import { formatDateTime } from "@/lib/format";
import { useApi } from "@/lib/use-api";

type TwoFactor = { enabled: boolean; enabled_at: string | null; recovery_codes_left: number; required: boolean };
type Session = { id: number; ip: string | null; user_agent: string | null; created_at: string; last_used_at: string; current: boolean; two_factor: boolean };

/** "Chrome on Windows" from a user-agent string (good enough to recognise your own devices). */
function device(ua: string | null) {
  if (!ua) return "Unknown device";
  const browser = /Edg\//.test(ua) ? "Edge" : /OPR\//.test(ua) ? "Opera" : /Chrome\//.test(ua) ? "Chrome" : /Firefox\//.test(ua) ? "Firefox" : /Safari\//.test(ua) ? "Safari" : "Browser";
  const os = /Android/.test(ua) ? "Android" : /iPhone|iPad/.test(ua) ? "iOS" : /Windows/.test(ua) ? "Windows" : /Mac OS X/.test(ua) ? "macOS" : /Linux/.test(ua) ? "Linux" : "";
  return os ? `${browser} on ${os}` : browser;
}

function useFormErrors() {
  const toast = useToast();
  const [errors, setErrors] = useState<Record<string, string>>({});
  const handle = (e: unknown) => {
    if (e instanceof ApiError && Object.keys(e.fields).length) setErrors(e.fields);
    else toast("error", (e as Error).message);
  };
  return { errors, setErrors, handle };
}

function PasswordPanel() {
  const toast = useToast();
  const { errors, setErrors, handle } = useFormErrors();
  const [busy, setBusy] = useState(false);
  async function submit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = e.currentTarget;
    const f = new FormData(form);
    if (f.get("password") !== f.get("confirm")) return setErrors({ confirm: "The two passwords don't match" });
    setBusy(true);
    setErrors({});
    try {
      await api.post("/auth/password", { current_password: f.get("current_password"), password: f.get("password") });
      form.reset();
      toast("success", "Password changed. Other devices have been signed out.");
    } catch (err) {
      handle(err);
    } finally {
      setBusy(false);
    }
  }
  return (
    <Panel className="p-5">
      <h2 className="font-semibold">Password</h2>
      <p className="mt-1 text-sm text-muted">Changing it signs you out on every other device.</p>
      <form onSubmit={submit} noValidate className="mt-4 grid max-w-xl gap-4">
        <Field label="Current password" htmlFor="pw-current" error={errors.current_password}>
          <Input id="pw-current" name="current_password" type="password" autoComplete="current-password" invalid={!!errors.current_password} />
        </Field>
        <div className="grid gap-4 sm:grid-cols-2">
          <Field label="New password" htmlFor="pw-new" error={errors.password} hint="10+ characters, a letter and a number">
            <Input id="pw-new" name="password" type="password" autoComplete="new-password" invalid={!!errors.password} />
          </Field>
          <Field label="Repeat new password" htmlFor="pw-confirm" error={errors.confirm}>
            <Input id="pw-confirm" name="confirm" type="password" autoComplete="new-password" invalid={!!errors.confirm} />
          </Field>
        </div>
        <div>
          <Button type="submit" variant="primary" loading={busy}>
            Change password
          </Button>
        </div>
      </form>
    </Panel>
  );
}

function RecoveryCodes({ codes }: { codes: string[] }) {
  const download = () => {
    const blob = new Blob([`Devwebs recovery codes (each works once)\n\n${codes.join("\n")}\n`], { type: "text/plain" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "devwebs-recovery-codes.txt";
    a.click();
    URL.revokeObjectURL(a.href);
  };
  return (
    <div>
      <ol aria-label="Recovery codes" className="tabular grid grid-cols-2 gap-x-6 gap-y-1.5 rounded-lg bg-paper p-4 font-mono text-[14px]">
        {codes.map((c) => (
          <li key={c}>{c}</li>
        ))}
      </ol>
      <Button className="mt-3" icon={<Download className="size-4" />} onClick={download}>
        Download as text
      </Button>
    </div>
  );
}

function SetupDialog({ onClose, onDone }: { onClose: () => void; onDone: () => void }) {
  const { errors, setErrors, handle } = useFormErrors();
  const [busy, setBusy] = useState(false);
  const [setup, setSetup] = useState<{ secret: string; otpauth_uri: string; qr: string } | null>(null);
  const [codes, setCodes] = useState<string[] | null>(null);

  async function start(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setBusy(true);
    setErrors({});
    try {
      const r = await api.post<{ data: { secret: string; otpauth_uri: string } }>("/auth/2fa/setup", { password: new FormData(e.currentTarget).get("password") });
      const qr = await QRCode.toDataURL(r.data.otpauth_uri, { margin: 1, width: 200, errorCorrectionLevel: "M" });
      setSetup({ ...r.data, qr });
    } catch (err) {
      handle(err);
    } finally {
      setBusy(false);
    }
  }
  async function confirm(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setBusy(true);
    setErrors({});
    try {
      const r = await api.post<{ data: { recovery_codes: string[] } }>("/auth/2fa/enable", { code: String(new FormData(e.currentTarget).get("code") ?? "").replace(/\s/g, "") });
      setCodes(r.data.recovery_codes);
    } catch (err) {
      handle(err);
    } finally {
      setBusy(false);
    }
  }

  if (codes) {
    return (
      <Dialog open onClose={onDone} title="Two-step sign-in is on" description="Save these recovery codes somewhere safe, like a password manager. If you lose your phone, each code lets you sign in once." footer={<Button variant="primary" onClick={onDone}>I&apos;ve saved them</Button>}>
        <RecoveryCodes codes={codes} />
      </Dialog>
    );
  }
  return (
    <Dialog
      open
      onClose={onClose}
      title="Turn on two-step sign-in"
      description={setup ? "Scan the code with an authenticator app (Google Authenticator, Microsoft Authenticator, 1Password, Authy…), then enter the 6-digit code it shows." : "First, confirm your password."}
      footer={
        <>
          <Button onClick={onClose}>Cancel</Button>
          <Button variant="primary" type="submit" form={setup ? "tfa-confirm" : "tfa-start"} loading={busy}>
            {setup ? "Turn on" : "Continue"}
          </Button>
        </>
      }
    >
      {!setup ? (
        <form id="tfa-start" onSubmit={start} noValidate>
          <Field label="Password" htmlFor="tfa-pw" error={errors.password}>
            <Input id="tfa-pw" name="password" type="password" autoComplete="current-password" invalid={!!errors.password} autoFocus />
          </Field>
        </form>
      ) : (
        <form id="tfa-confirm" onSubmit={confirm} noValidate className="space-y-4">
          <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-start">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img src={setup.qr} alt="QR code to add Devwebs to your authenticator app" width={176} height={176} className="rounded-lg border border-line" />
            <div className="min-w-0 flex-1 text-sm">
              <p className="mb-1.5 text-muted">Can&apos;t scan? Enter this key in the app instead:</p>
              <CopyField value={setup.secret.replace(/(.{4})/g, "$1 ").trim()} label="Setup key" />
            </div>
          </div>
          <Field label="6-digit code from the app" htmlFor="tfa-code" error={errors.code}>
            <Input id="tfa-code" name="code" inputMode="numeric" autoComplete="one-time-code" maxLength={7} className="tabular w-40 tracking-widest" invalid={!!errors.code} autoFocus />
          </Field>
        </form>
      )}
    </Dialog>
  );
}

function TwoFactorPanel() {
  const toast = useToast();
  const { refresh } = useSession();
  const { data, reload } = useApi<{ data: TwoFactor }>("/auth/2fa");
  const [dialog, setDialog] = useState<"setup" | "disable" | "codes" | null>(null);
  const [codes, setCodes] = useState<string[] | null>(null);
  const { errors, setErrors, handle } = useFormErrors();
  const [busy, setBusy] = useState(false);
  const t = data?.data;

  async function disable(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const f = new FormData(e.currentTarget);
    setBusy(true);
    setErrors({});
    try {
      await api.post("/auth/2fa/disable", { password: f.get("password"), code: f.get("code") });
      toast("success", "Two-step sign-in turned off");
      setDialog(null);
      reload();
      refresh();
    } catch (err) {
      handle(err);
    } finally {
      setBusy(false);
    }
  }
  async function regenerate(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setBusy(true);
    setErrors({});
    try {
      const r = await api.post<{ data: { recovery_codes: string[] } }>("/auth/2fa/recovery-codes", { password: new FormData(e.currentTarget).get("password") });
      setCodes(r.data.recovery_codes);
      reload();
    } catch (err) {
      handle(err);
    } finally {
      setBusy(false);
    }
  }

  return (
    <Panel className="p-5">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h2 className="flex items-center gap-2 font-semibold">
            Two-step sign-in {t && <Badge tone={t.enabled ? "teal" : "amber"}>{t.enabled ? "On" : "Off"}</Badge>}
          </h2>
          <p className="mt-1 max-w-xl text-sm text-muted">
            After your password, Devwebs asks for a code from an authenticator app on your phone, so a stolen password alone can&apos;t open your account.
            {t?.required ? " It is required for Devwebs staff accounts." : ""}
          </p>
          {t?.enabled && (
            <p className="mt-1 text-sm text-muted">
              On since {formatDateTime(t.enabled_at)}; {t.recovery_codes_left} recovery code{t.recovery_codes_left === 1 ? "" : "s"} left.
            </p>
          )}
        </div>
        {!t ? (
          <Skeleton className="h-9 w-32" />
        ) : t.enabled ? (
          <div className="flex flex-wrap gap-2">
            <Button onClick={() => (setErrors({}), setCodes(null), setDialog("codes"))}>New recovery codes</Button>
            {!t.required && (
              <Button variant="ghost" className="hover:bg-brick-soft hover:text-brick" onClick={() => (setErrors({}), setDialog("disable"))}>
                Turn off
              </Button>
            )}
          </div>
        ) : (
          <Button variant="primary" icon={<ShieldCheck className="size-4" />} onClick={() => setDialog("setup")}>
            Turn on
          </Button>
        )}
      </div>
      {dialog === "setup" && (
        <SetupDialog
          onClose={() => setDialog(null)}
          onDone={() => {
            setDialog(null);
            reload();
            refresh();
            toast("success", "Two-step sign-in is on");
          }}
        />
      )}
      {dialog === "disable" && (
        <Dialog open onClose={() => setDialog(null)} title="Turn off two-step sign-in?" description="Your account will be protected by your password only." footer={<><Button onClick={() => setDialog(null)}>Cancel</Button><Button variant="danger" type="submit" form="tfa-off" loading={busy}>Turn off</Button></>}>
          <form id="tfa-off" onSubmit={disable} noValidate className="space-y-4">
            <Field label="Password" htmlFor="off-pw" error={errors.password}>
              <Input id="off-pw" name="password" type="password" autoComplete="current-password" invalid={!!errors.password} />
            </Field>
            <Field label="Code from the app, or a recovery code" htmlFor="off-code" error={errors.code}>
              <Input id="off-code" name="code" autoComplete="one-time-code" invalid={!!errors.code} />
            </Field>
          </form>
        </Dialog>
      )}
      {dialog === "codes" && (
        <Dialog
          open
          onClose={() => setDialog(null)}
          title="New recovery codes"
          description={codes ? "Your old codes no longer work. Save these somewhere safe." : "This replaces all your current recovery codes."}
          footer={codes ? <Button variant="primary" onClick={() => setDialog(null)}>I&apos;ve saved them</Button> : <><Button onClick={() => setDialog(null)}>Cancel</Button><Button variant="primary" type="submit" form="tfa-codes" loading={busy}>Create new codes</Button></>}
        >
          {codes ? (
            <RecoveryCodes codes={codes} />
          ) : (
            <form id="tfa-codes" onSubmit={regenerate} noValidate>
              <Field label="Password" htmlFor="codes-pw" error={errors.password}>
                <Input id="codes-pw" name="password" type="password" autoComplete="current-password" invalid={!!errors.password} autoFocus />
              </Field>
            </form>
          )}
        </Dialog>
      )}
    </Panel>
  );
}

function SessionsPanel() {
  const toast = useToast();
  const { data, reload } = useApi<{ data: Session[] }>("/auth/sessions");
  const [confirm, setConfirm] = useState(false);
  const [busy, setBusy] = useState(false);
  async function revoke(id: number) {
    try {
      await api.delete(`/auth/sessions/${id}`);
      toast("success", "Signed out that device");
      reload();
    } catch (e) {
      toast("error", (e as Error).message);
    }
  }
  async function revokeOthers() {
    setBusy(true);
    try {
      const r = await api.post<{ revoked: number }>("/auth/sessions/revoke-others");
      toast("success", `Signed out ${r.revoked} other device${r.revoked === 1 ? "" : "s"}`);
      setConfirm(false);
      reload();
    } finally {
      setBusy(false);
    }
  }
  const others = data?.data.filter((s) => !s.current).length ?? 0;
  return (
    <Panel>
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-line px-5 py-3">
        <h2 className="font-semibold">Where you&apos;re signed in</h2>
        {others > 0 && <Button size="sm" onClick={() => setConfirm(true)}>Sign out all other devices</Button>}
      </div>
      {!data ? (
        <Skeleton className="m-5 h-24" />
      ) : (
        <ul className="divide-y divide-line">
          {data.data.map((s) => (
            <li key={s.id} className="flex flex-wrap items-center gap-x-4 gap-y-1 px-5 py-3 text-sm">
              <Laptop className="size-5 shrink-0 text-muted" />
              <div className="min-w-0 flex-1 basis-56">
                <p className="font-medium">
                  {device(s.user_agent)} {s.current && <Badge tone="teal">This device</Badge>}
                </p>
                <p className="text-[13px] text-muted">
                  {s.ip ?? "Unknown address"}, signed in {formatDateTime(s.created_at)}, last active {formatDateTime(s.last_used_at)}
                </p>
              </div>
              {!s.current && (
                <Button size="sm" variant="ghost" onClick={() => revoke(s.id)}>
                  Sign out
                </Button>
              )}
            </li>
          ))}
        </ul>
      )}
      <ConfirmDialog open={confirm} title="Sign out all other devices?" message="Every other browser and phone signed in to your account will need to sign in again." confirmLabel="Sign them out" loading={busy} onConfirm={revokeOthers} onClose={() => setConfirm(false)} />
    </Panel>
  );
}

export default function AccountSecurityPage() {
  const { me } = useSession();
  return (
    <>
      <PageHeader title="Account security" description={me ? `Sign-in settings for ${me.user.email}. They apply to every workspace you belong to.` : "Your password, two-step sign-in and devices."} />
      <div className="space-y-6">
        <TwoFactorPanel />
        <PasswordPanel />
        <SessionsPanel />
      </div>
    </>
  );
}
