"use client"

import * as React from "react"
import Link from "next/link"

import { Bilingual } from "@/components/bilingual"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"
import { translator } from "@/lib/i18n"
import { cn } from "@/lib/utils"
import { USERNAME_MIN, checkUsername, normaliseUsername } from "@/lib/username"
import { attemptSignIn } from "@/app/login/actions"
import type { SignInOutcome } from "@/lib/services/sign-in"
import {
  IconAlertTriangle,
  IconCircleCheck,
  IconEye,
  IconEyeOff,
} from "@tabler/icons-react"

/**
 * Signing in by username, given to somebody when they are hired.
 *
 * Not by employee code, which this asked for until 4 August. The client's
 * answer to their own question 10 is that the code is an internal identifier
 * for syncing with the accounting database and never the login. Both live in
 * the system and the person links them.
 *
 * The field folds to lower case as it is typed rather than refusing capitals
 * later. Caps lock on a shared terminal is not a mistake worth a red message,
 * and `N.Kulisra` and `n.kulisra` must never be able to become two people.
 *
 * There is no sign-up link and there will never be one. An admin creates the
 * account. What replaces it is the link to the help screen, which is the only
 * thing a person who cannot get in actually needs.
 *
 * What can be answered without asking the server is answered here: an empty
 * field, and a code that is not yet eight digits. Everything else is a question
 * only the roster can answer, and it goes to `attemptSignIn`.
 */

type Outcome =
  | { kind: "idle" }
  | { kind: "code"; en: string; th: string }
  | { kind: "password"; en: string; th: string }
  /**
   * Neither field is wrong, so neither goes red and the cursor stays where it
   * is. Sending somebody back into the password box would be telling them to
   * type again, which is the one thing that will not work for the next few
   * minutes.
   */
  | { kind: "blocked"; en: string; th: string }
  | { kind: "accepted" }

/** The half of the server's answer that is a refusal. */
type Refusal = Extract<SignInOutcome, { ok: false }>

/**
 * A wrong code is a wrong code. Nothing here tells somebody they are not
 * allowed in: they are allowed in, they mistyped, and the message says what
 * to do about it and who can look the code up.
 */
function judgeLocally(code: string, password: string): Outcome | null {
  if (code.length === 0) {
    return {
      kind: "code",
      en: "Type your username, the one Admin or HR gave you.",
      th: "กรุณากรอกชื่อผู้ใช้ที่ฝ่ายธุรการหรือฝ่ายบุคคลให้ไว้",
    }
  }

  /*
    Only the shape, and only the part a browser can be sure of. Whether this
    name belongs to anybody is a question for the roster, and guessing it here
    would mean two places deciding who exists.
  */
  const problem = checkUsername(code)
  if (problem === "too_short") {
    return {
      kind: "code",
      en: `A username is at least ${USERNAME_MIN} characters. This one has ${code.length}.`,
      th: `ชื่อผู้ใช้ต้องมีอย่างน้อย ${USERNAME_MIN} ตัวอักษร ที่กรอกไว้มี ${code.length} ตัว`,
    }
  }
  if (problem === "bad_characters" || problem === "too_long") {
    return {
      kind: "code",
      en: "That is not a username. Letters and numbers, and it may hold a dot, a hyphen or an underscore.",
      th: "ชื่อผู้ใช้ไม่ถูกต้อง ใช้ตัวอักษรและตัวเลข และอาจมีจุด ขีดกลาง หรือขีดล่างได้",
    }
  }

  if (password.length === 0) {
    return {
      kind: "password",
      en: "Type your password.",
      th: "กรุณากรอกรหัสผ่าน",
    }
  }

  // Whether this pair opens anything is not a question the browser can answer.
  return null
}

/** What each answer from the server reads as, in both languages. */
function judgeRemotely(refusal: Refusal): Outcome {
  switch (refusal.reason) {
    case "unknown_user":
      return {
        kind: "code",
        en: "That username is not on file here. Check the spelling, or ask Admin or HR to look it up for you.",
        th: "ไม่พบชื่อผู้ใช้นี้ในระบบ กรุณาตรวจสอบตัวสะกด หรือติดต่อฝ่ายธุรการหรือฝ่ายบุคคลให้ช่วยตรวจสอบ",
      }
    // The common case, not the exotic one: most of the roster has no login yet.
    // Telling these people their password is wrong would send them hunting for
    // a password nobody ever gave them.
    case "no_login":
      return {
        kind: "code",
        en: "This username is on file, but no account has been opened for it yet. Ask Admin or HR to open one.",
        th: "พบชื่อผู้ใช้นี้ในระบบ แต่ยังไม่ได้เปิดบัญชี กรุณาติดต่อฝ่ายธุรการหรือฝ่ายบุคคลเพื่อเปิดบัญชี",
      }
    // Not "you are not allowed in", which reads as an accusation. They worked
    // here and they do not any more, and the person who can say more about that
    // is HR rather than this screen.
    case "archived":
      return {
        kind: "code",
        en: "This account is no longer active. Ask Admin or HR if you think that is wrong.",
        th: "บัญชีนี้ถูกปิดการใช้งานแล้ว หากคิดว่าไม่ถูกต้อง กรุณาติดต่อฝ่ายธุรการหรือฝ่ายบุคคล",
      }
    case "wrong_password":
      return {
        kind: "password",
        en: "That password does not go with this username. Try again, or ask Admin or HR to set a new one.",
        th: "รหัสผ่านไม่ตรงกับชื่อผู้ใช้นี้ กรุณาลองใหม่ หรือติดต่อฝ่ายธุรการหรือฝ่ายบุคคลเพื่อตั้งรหัสผ่านใหม่",
      }
    // The only one of the five that comes from the dictionary rather than being
    // written here, because it carries a number. `translator` puts the value
    // into both languages from one call.
    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))
      const key =
        minutes === 1
          ? "signIn.tooManyAttemptsOneMinute"
          : "signIn.tooManyAttempts"
      return {
        kind: "blocked",
        en: translator("en")(key, { minutes }),
        th: translator("th")(key, { minutes }),
      }
    }
  }
}

function Message({
  tone,
  en,
  th,
  id,
}: {
  tone: "error" | "accepted"
  en: string
  th: string
  id?: string
}) {
  const Icon = tone === "error" ? IconAlertTriangle : IconCircleCheck

  return (
    <FieldError
      id={id}
      className={cn(
        "flex items-start gap-2 text-base",
        tone === "accepted" && "text-foreground",
      )}
    >
      <Icon className="mt-0.5 size-5 shrink-0" aria-hidden />
      <Bilingual
        en={en}
        th={th}
        thClassName={tone === "error" ? "text-inherit" : undefined}
      />
    </FieldError>
  )
}

export function SignInForm() {
  const [code, setCode] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [visible, setVisible] = React.useState(false)
  const [outcome, setOutcome] = React.useState<Outcome>({ kind: "idle" })
  const [pending, setPending] = React.useState(false)

  const codeRef = React.useRef<HTMLInputElement>(null)
  const passwordRef = React.useRef<HTMLInputElement>(null)

  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 rather than tabbing
    // back up the form.
    const land = (verdict: Outcome) => {
      setOutcome(verdict)
      if (verdict.kind === "code") codeRef.current?.focus()
      if (verdict.kind === "password") passwordRef.current?.focus()
    }

    const local = judgeLocally(code, password)
    if (local) {
      land(local)
      return
    }

    setPending(true)
    try {
      const result = await attemptSignIn(code, password)
      if (result.ok) {
        setOutcome({ kind: "accepted" })
        // A full navigation and not `router.push`: the session cookie was just
        // set, and everything on the other side is rendered on the server from
        // that cookie.
        // Everybody lands on their own task list. routes.json redirects "/" to
        // "/tasks", and there is no per-tier landing: the three placeholder
        // dashboards that used to be here appear nowhere in the specification.
        window.location.assign("/tasks")
        return
      }
      land(judgeRemotely(result))
    } catch {
      land({
        kind: "password",
        en: "The system did not answer. Try again in a moment, or tell Admin.",
        th: "ระบบไม่ตอบสนอง กรุณาลองใหม่อีกครั้ง หรือแจ้งฝ่ายธุรการ",
      })
    } finally {
      setPending(false)
    }
  }

  const codeError = outcome.kind === "code" ? outcome : null
  const passwordError = outcome.kind === "password" ? outcome : null

  return (
    <div className="flex flex-col gap-6">
      <Card>
        <CardContent>
          <form onSubmit={submit} noValidate className="flex flex-col gap-6">
            <FieldGroup>
              {/*
                The field is not marked `data-invalid`: that turns every
                descendant destructive, including the label and the digits
                somebody just typed. A value nobody can read comfortably is
                the wrong way to say a value is wrong. The ring on the input,
                the icon and the words carry it instead.
              */}
              <Field>
                <FieldLabel htmlFor="username" className="text-base">
                  <Bilingual en="Username" th="ชื่อผู้ใช้" />
                </FieldLabel>
                <Input
                  id="username"
                  name="username"
                  ref={codeRef}
                  value={code}
                  // Folded as it is typed rather than refused afterwards. Caps
                  // lock on a shared terminal is not a mistake worth a message.
                  onChange={(event) =>
                    setCode(normaliseUsername(event.target.value))
                  }
                  autoComplete="username"
                  autoCapitalize="off"
                  autoCorrect="off"
                  spellCheck={false}
                  autoFocus
                  aria-invalid={codeError ? true : undefined}
                  aria-describedby={
                    codeError ? "username-message" : "username-hint"
                  }
                  className="h-12 text-lg font-medium md:text-lg"
                />
                {codeError ? (
                  <Message
                    id="username-message"
                    tone="error"
                    en={codeError.en}
                    th={codeError.th}
                  />
                ) : (
                  <FieldDescription
                    id="username-hint"
                    className="text-base text-foreground/80"
                  >
                    <Bilingual
                      en="The name Admin or HR gave you. Not your employee code."
                      th="ชื่อผู้ใช้ที่ฝ่ายธุรการหรือฝ่ายบุคคลให้ไว้ ไม่ใช่รหัสพนักงาน"
                    />
                  </FieldDescription>
                )}
              </Field>

              <Field>
                <FieldLabel htmlFor="password" className="text-base">
                  <Bilingual en="Password" th="รหัสผ่าน" />
                </FieldLabel>
                <InputGroup className="h-12">
                  <InputGroupInput
                    id="password"
                    name="password"
                    ref={passwordRef}
                    type={visible ? "text" : "password"}
                    value={password}
                    onChange={(event) => setPassword(event.target.value)}
                    autoComplete="current-password"
                    autoCapitalize="off"
                    autoCorrect="off"
                    spellCheck={false}
                    aria-invalid={passwordError ? true : undefined}
                    aria-describedby={
                      passwordError ? "password-message" : undefined
                    }
                    className="h-full text-base md:text-base"
                  />
                  <InputGroupAddon align="inline-end">
                    <InputGroupButton
                      size="icon-sm"
                      className="size-10"
                      onClick={() => setVisible((shown) => !shown)}
                      aria-pressed={visible}
                    >
                      {visible ? (
                        <IconEyeOff className="size-5" />
                      ) : (
                        <IconEye className="size-5" />
                      )}
                      <span className="sr-only">
                        {visible
                          ? "Hide password, ซ่อนรหัสผ่าน"
                          : "Show password, แสดงรหัสผ่าน"}
                      </span>
                    </InputGroupButton>
                  </InputGroupAddon>
                </InputGroup>
                {passwordError ? (
                  <Message
                    id="password-message"
                    tone="error"
                    en={passwordError.en}
                    th={passwordError.th}
                  />
                ) : null}
              </Field>
            </FieldGroup>

            {/*
              Disabled while the answer is on its way, and the label says so
              rather than the button going quiet and looking broken. Nothing
              spins: a factory terminal on a slow network shows a spinner often
              enough that it stops meaning anything.
            */}
            <Button
              type="submit"
              size="lg"
              disabled={pending}
              className="h-12 w-full text-base"
            >
              {pending
                ? "Signing in, กำลังเข้าสู่ระบบ"
                : "Sign in, เข้าสู่ระบบ"}
            </Button>

            {outcome.kind === "blocked" ? (
              <Message tone="error" en={outcome.en} th={outcome.th} />
            ) : null}

            {outcome.kind === "accepted" ? (
              <Message
                tone="accepted"
                en="Signed in. Taking you to your screen."
                th="เข้าสู่ระบบแล้ว กำลังพาไปยังหน้าจอของคุณ"
              />
            ) : null}
          </form>
        </CardContent>
      </Card>

      <Link
        href="/login/help"
        className="flex justify-center rounded-lg px-4 py-3 text-center underline-offset-4 hover:bg-muted hover:underline focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
      >
        <Bilingual
          en="Cannot sign in?"
          th="เข้าสู่ระบบไม่ได้ใช่ไหม"
          className="items-center"
        />
      </Link>
    </div>
  )
}
