"use client"

import * as React from "react"

import { useT } from "@/components/locale-provider"
import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { PASSWORD_MIN, checkNewPassword } from "@/lib/password"
import { cn } from "@/lib/utils"
import { changePassword } from "@/app/settings/password"
import type { PasswordChange } from "@/lib/services/change-password"
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react"

/**
 * Replacing your own password, which until this screen existed nobody could do
 * without asking HR for an operation that is none of HR's business.
 *
 * The person stays signed in. Everywhere else they were signed in does not:
 * `changeOwnPassword` says why, and `settings.changed` tells them so, because
 * somebody who walks to the other terminal and finds it logged out deserves to
 * have been warned rather than to think something broke.
 *
 * What the browser can decide it decides here: a password shorter than the
 * minimum, and two new ones that differ. Both are asked again on the server,
 * where the answer counts. Nothing else is guessed at: whether the current
 * password is the current password is not a question this file can answer.
 */

/** Which field the message belongs under, or the whole form. */
type Outcome =
  | { kind: "idle" }
  | { kind: "current"; message: string }
  | { kind: "new"; message: string }
  | { kind: "again"; message: string }
  /**
   * Neither field is wrong, so none goes red and the cursor stays where it is.
   * Sending somebody back into a box would be telling them to type again, which
   * is the one thing that will not work for the next few minutes.
   */
  | { kind: "blocked"; message: string }
  | { kind: "done" }

function Note({
  tone,
  message,
  id,
}: {
  tone: "error" | "done"
  message: string
  id?: string
}) {
  const Icon = tone === "error" ? IconAlertTriangle : IconCircleCheck

  return (
    <FieldError
      id={id}
      className={cn(
        "flex items-start gap-2",
        // Success is not a failure wearing a different icon. `FieldError` is
        // borrowed for its live region, which is what makes a screen reader
        // announce either answer without the focus having to move.
        tone === "done" && "text-foreground",
      )}
    >
      <Icon className="mt-0.5 size-4 shrink-0" aria-hidden />
      <span>{message}</span>
    </FieldError>
  )
}

export function PasswordForm() {
  const t = useT()
  const [current, setCurrent] = React.useState("")
  const [next, setNext] = React.useState("")
  const [again, setAgain] = React.useState("")
  const [outcome, setOutcome] = React.useState<Outcome>({ kind: "idle" })
  const [pending, setPending] = React.useState(false)

  const currentRef = React.useRef<HTMLInputElement>(null)
  const nextRef = React.useRef<HTMLInputElement>(null)
  const againRef = React.useRef<HTMLInputElement>(null)

  /**
   * What each refusal reads as. The server sends a reason and never a sentence,
   * so a screen in Thai says it in Thai from one dictionary.
   */
  function read(refusal: Extract<PasswordChange, { ok: false }>): Outcome {
    switch (refusal.reason) {
      case "wrong_password":
        return { kind: "current", message: t("settings.wrongPassword") }
      case "too_short":
        return {
          kind: "new",
          message: t("settings.tooShort", { min: PASSWORD_MIN }),
        }
      case "mismatch":
        return { kind: "again", message: t("settings.mismatch") }
      case "too_many_attempts": {
        // Whole minutes, rounded up, never zero. Seconds invite somebody to sit
        // and count, and the figure is stale the moment it reaches the screen.
        const minutes = Math.max(1, Math.ceil(refusal.retryAfter / 60))
        return {
          kind: "blocked",
          message: t(
            minutes === 1
              ? "settings.tooManyAttemptsOneMinute"
              : "settings.tooManyAttempts",
            { minutes },
          ),
        }
      }
      case "failed":
        return { kind: "blocked", message: t("settings.notChanged") }
    }
  }

  async function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()

    // Send the cursor to whichever field the message is about, so somebody
    // working from the keyboard fixes it where they are.
    const land = (verdict: Outcome) => {
      setOutcome(verdict)
      if (verdict.kind === "current") currentRef.current?.focus()
      if (verdict.kind === "new") nextRef.current?.focus()
      if (verdict.kind === "again") againRef.current?.focus()
    }

    const problem = checkNewPassword(next, again)
    if (problem) {
      land(
        problem === "too_short"
          ? {
              kind: "new",
              message: t("settings.tooShort", { min: PASSWORD_MIN }),
            }
          : { kind: "again", message: t("settings.mismatch") },
      )
      return
    }

    setPending(true)
    try {
      const result = await changePassword(current, next, again)
      if (result.ok) {
        // Emptied, and all three of them. The new password is as much worth
        // clearing as the old one: this is a shared terminal and the next
        // person to walk up gets a form with nothing in it.
        setCurrent("")
        setNext("")
        setAgain("")
        setOutcome({ kind: "done" })
        return
      }
      land(read(result))
    } catch {
      land({ kind: "blocked", message: t("settings.notChanged") })
    } finally {
      setPending(false)
    }
  }

  const currentError = outcome.kind === "current" ? outcome.message : null
  const nextError = outcome.kind === "new" ? outcome.message : null
  const againError = outcome.kind === "again" ? outcome.message : null

  // A courtesy and not a check: the action is reachable without this form, and
  // `changeOwnPassword` asks the same questions again where the answer counts.
  const ready =
    current.length > 0 && next.length > 0 && again.length > 0 && !pending

  return (
    <form onSubmit={submit} noValidate className="flex flex-col gap-6">
      <FieldGroup>
        <Field>
          <FieldLabel htmlFor="current-password">
            {t("settings.currentPassword")}
          </FieldLabel>
          <Input
            id="current-password"
            name="current-password"
            type="password"
            ref={currentRef}
            value={current}
            onChange={(event) => setCurrent(event.target.value)}
            autoComplete="current-password"
            autoCapitalize="off"
            autoCorrect="off"
            spellCheck={false}
            aria-invalid={currentError ? true : undefined}
            aria-describedby={
              currentError ? "current-password-message" : undefined
            }
          />
          {currentError ? (
            <Note
              id="current-password-message"
              tone="error"
              message={currentError}
            />
          ) : null}
        </Field>

        <Field>
          <FieldLabel htmlFor="new-password">
            {t("settings.newPassword")}
          </FieldLabel>
          <Input
            id="new-password"
            name="new-password"
            type="password"
            ref={nextRef}
            value={next}
            onChange={(event) => setNext(event.target.value)}
            autoComplete="new-password"
            autoCapitalize="off"
            autoCorrect="off"
            spellCheck={false}
            aria-invalid={nextError ? true : undefined}
            aria-describedby={
              nextError ? "new-password-message" : "new-password-hint"
            }
          />
          {nextError ? (
            <Note id="new-password-message" tone="error" message={nextError} />
          ) : (
            // The number comes from the constant the server enforces, so the
            // hint cannot promise something the endpoint then refuses.
            <FieldDescription id="new-password-hint">
              {t("settings.minLength", { min: PASSWORD_MIN })}
            </FieldDescription>
          )}
        </Field>

        <Field>
          <FieldLabel htmlFor="repeat-password">
            {t("settings.repeatPassword")}
          </FieldLabel>
          <Input
            id="repeat-password"
            name="repeat-password"
            type="password"
            ref={againRef}
            value={again}
            onChange={(event) => setAgain(event.target.value)}
            autoComplete="new-password"
            autoCapitalize="off"
            autoCorrect="off"
            spellCheck={false}
            aria-invalid={againError ? true : undefined}
            aria-describedby={
              againError ? "repeat-password-message" : undefined
            }
          />
          {againError ? (
            <Note
              id="repeat-password-message"
              tone="error"
              message={againError}
            />
          ) : null}
        </Field>
      </FieldGroup>

      <Button type="submit" disabled={!ready} className="self-start">
        {pending ? t("settings.changing") : t("settings.change")}
      </Button>

      {outcome.kind === "blocked" ? (
        <Note tone="error" message={outcome.message} />
      ) : null}

      {outcome.kind === "done" ? (
        <Note tone="done" message={t("settings.changed")} />
      ) : null}
    </form>
  )
}
