'use client';

import {useCallback, useState} from 'react';
import {useRouter, useSearchParams} from 'next/navigation';
import {useToast} from '@astryxdesign/core/Toast';
import {useSession} from '@/features/auth/providers/SessionProvider';
import {
  LOGIN_ERROR_PARAM,
  loginErrorFromCode,
} from '@/features/auth/services/loginErrorCodes';
import {resolveRedirectTarget} from '@/features/auth/services/redirectTarget';
import type {LoginError} from '@/features/auth/types/auth';

/**
 * Where credentials are POSTed. Exported because the form element itself needs
 * it for `action` — the no-JavaScript path submits straight here, bypassing
 * this hook, authService and the repository entirely.
 */
export const LOGIN_ENDPOINT = '/api/auth/login';

/**
 * Every behaviour of the sign-in form, with no markup attached.
 *
 * Pulled out of the component for the reason the brief asks for: the form was
 * a 314-line file where a fake `setTimeout` login sat in the middle of layout
 * markup. Now the view renders state and the rules live here — which also
 * makes the flow testable without mounting a page, and means a redesign of the
 * screen cannot silently change what "signed in" means.
 *
 * The chain below this hook is: SessionProvider → authService (validation and
 * domain rules) → authRepository (transport) → POST /api/auth/login. This hook
 * knows about none of it beyond the first link.
 */

export interface LoginFormState {
  email: string;
  password: string;
  rememberMe: boolean;
  /** Field-addressed, so a message renders under the input that caused it. */
  errors: Partial<Record<LoginError['field'], string>>;
  isSubmitting: boolean;
}

export function useLoginForm() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const toast = useToast();
  const {login} = useSession();

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [rememberMe, setRememberMe] = useState(false);
  /**
   * Seeded from `?error=` so a sign-in that failed on the no-JavaScript path
   * still shows its message once the page hydrates. The code is looked up in a
   * fixed table — an unrecognised one yields nothing, so the URL cannot be
   * used to write arbitrary text onto the sign-in screen.
   *
   * Seeding in useState rather than an effect keeps the message present in the
   * first render, server and client alike, so there is no flash and no
   * hydration mismatch to patch up.
   */
  const [errors, setErrors] = useState<LoginFormState['errors']>(() => {
    const seeded = loginErrorFromCode(searchParams.get(LOGIN_ERROR_PARAM));
    return seeded ? {[seeded.field]: seeded.message} : {};
  });
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Raw, for the hidden field the native form path posts back. Unvalidated
  // here on purpose: the server validates it through resolveRedirectTarget,
  // which is the only side that can be trusted to.
  const next = searchParams.get('next');

  // Shared with GuestGuard, which redirects on the same event — see
  // resolveRedirectTarget for why that matters and how `next` is validated.
  const destination = resolveRedirectTarget(next);

  const submit = useCallback(
    async (event: React.FormEvent) => {
      event.preventDefault();
      if (isSubmitting) return;

      setErrors({});
      setIsSubmitting(true);

      const result = await login({email, password, rememberMe});

      if (!result.ok) {
        setErrors({[result.error.field]: result.error.message});
        // The toast carries what the inline message cannot: it is announced,
        // it survives a field the user has scrolled past, and it distinguishes
        // "we rejected your credentials" from "we never reached the server".
        toast({type: 'error', body: result.error.message});
        setIsSubmitting(false);
        return;
      }

      // Deliberately NOT clearing isSubmitting on success: the button stays in
      // its loading state until the navigation commits, so the form cannot be
      // submitted twice while the route transition is in flight.
      router.replace(destination);
      // The session cookie changed, so any server-rendered layout above this
      // route is stale. Without this, the workspace can paint its signed-out
      // seed until something else happens to refetch.
      router.refresh();
    },
    [
      isSubmitting,
      login,
      email,
      password,
      rememberMe,
      toast,
      router,
      destination,
    ],
  );

  /** Clearing on edit: an error that outlives the typo it describes is noise. */
  const updateEmail = useCallback((value: string) => {
    setEmail(value);
    setErrors((prev) => (prev.email ? {...prev, email: undefined} : prev));
  }, []);

  const updatePassword = useCallback((value: string) => {
    setPassword(value);
    setErrors((prev) =>
      prev.password ? {...prev, password: undefined} : prev,
    );
  }, []);

  return {
    email,
    password,
    rememberMe,
    errors,
    isSubmitting,
    next,
    setEmail: updateEmail,
    setPassword: updatePassword,
    setRememberMe,
    submit,
  };
}
