/**
 * Framework-agnostic Zod form utilities for React environments.
 * Provides predictable error and touched state handling.
 */

import { useCallback, useMemo, useState } from "react";
import type { FormEvent } from "react";
import { ZodError, type ZodIssue, type ZodSchema } from "zod";

export type FormErrors<_TValues extends Record<string, unknown>> = Record<
  string,
  string | undefined
>;
export type FormTouched<_TValues extends Record<string, unknown>> = Record<
  string,
  boolean | undefined
>;

export interface ZodFormOptions<TValues extends Record<string, unknown>> {
  schema: ZodSchema<TValues>;
  initialValues: TValues;
  onSubmit?: (data: TValues) => Promise<void> | void;
}

export interface UseZodFormReturn<TValues extends Record<string, unknown>> {
  values: TValues;
  errors: FormErrors<TValues>;
  touched: FormTouched<TValues>;
  submitError: string | null;
  isSubmitting: boolean;
  isValid: boolean;
  setValue: <K extends keyof TValues>(field: K, value: TValues[K]) => void;
  setTouched: <K extends keyof TValues>(field: K, touched: boolean) => void;
  setTouchedField: <K extends keyof TValues>(field: K, touched?: boolean) => void;
  validate: () => boolean;
  validateField: <K extends keyof TValues>(field: K) => boolean;
  handleSubmit: (event?: FormEvent) => Promise<void>;
  reset: () => void;
}

function issuesToErrors<TValues extends Record<string, unknown>>(
  issues: ZodIssue[]
): FormErrors<TValues> {
  const nextErrors: FormErrors<TValues> = {};

  issues.forEach(issue => {
    const [first, ...rest] = issue.path;
    const key = issue.path.join(".");

    if (typeof first === "string" && nextErrors[first] === undefined) {
      nextErrors[first] = issue.message;
    }

    if (key) {
      nextErrors[key] = issue.message;

      if (rest.length > 0) {
        const topLevelKey = String(first);
        if (nextErrors[topLevelKey] === undefined) {
          nextErrors[topLevelKey] = issue.message;
        }
      }
    } else if (nextErrors._form === undefined) {
      nextErrors._form = issue.message;
    }
  });

  return nextErrors;
}

export function useZodForm<TValues extends Record<string, unknown>>({
  schema,
  initialValues,
  onSubmit,
}: ZodFormOptions<TValues>): UseZodFormReturn<TValues> {
  const [values, setValues] = useState<TValues>(initialValues);
  const [errors, setErrors] = useState<FormErrors<TValues>>({});
  const [touched, setTouchedState] = useState<FormTouched<TValues>>({});
  const [submitError, setSubmitError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const clearFieldError = useCallback((field: keyof TValues) => {
    const fieldKey = String(field);
    setErrors(prev => {
      const prefix = `${fieldKey}.`;
      const hasDirectError = prev[fieldKey] !== undefined;
      const hasNestedError = Object.keys(prev).some(key => key.startsWith(prefix));

      if (!hasDirectError && !hasNestedError) {
        return prev;
      }

      const next: FormErrors<TValues> = { ...prev };
      delete next[fieldKey];
      Object.keys(next).forEach(key => {
        if (key.startsWith(prefix)) {
          delete next[key];
        }
      });
      return next;
    });
  }, []);

  const validate = useCallback((): boolean => {
    try {
      schema.parse(values);
      setErrors({});
      return true;
    } catch (error) {
      if (error instanceof ZodError) {
        setErrors(issuesToErrors<TValues>(error.issues));
      }
      return false;
    }
  }, [schema, values]);

  const validateField = useCallback(
    <K extends keyof TValues>(field: K): boolean => {
      const result = schema.safeParse(values);

      if (result.success) {
        clearFieldError(field);
        setErrors(prev => {
          if (prev._form === undefined) {
            return prev;
          }
          const next: FormErrors<TValues> = { ...prev };
          delete next._form;
          return next;
        });
        return true;
      }

      const fieldKey = String(field);
      const relatedIssues = result.error.issues.filter(issue => issue.path[0] === field);

      setErrors(prev => {
        const next: FormErrors<TValues> = { ...prev };

        if (relatedIssues.length > 0) {
          const message = relatedIssues[0]?.message ?? "";
          next[fieldKey] = message;
          relatedIssues.forEach(issue => {
            const nestedKey = issue.path.join(".");
            if (nestedKey) {
              next[nestedKey] = issue.message;
            }
          });
        } else {
          delete next[fieldKey];
        }

        const formLevelIssue = result.error.issues.find(issue => issue.path.length === 0);
        if (formLevelIssue) {
          next._form = formLevelIssue.message;
        } else if (relatedIssues.length === 0) {
          delete next._form;
        }

        return next;
      });

      return relatedIssues.length === 0;
    },
    [schema, values, clearFieldError]
  );

  const setValue = useCallback(
    <K extends keyof TValues>(field: K, value: TValues[K]): void => {
      setValues(prev => ({ ...prev, [field]: value }));
      clearFieldError(field);
    },
    [clearFieldError]
  );

  const setTouched = useCallback(<K extends keyof TValues>(field: K, value: boolean): void => {
    setTouchedState(prev => ({ ...prev, [String(field)]: value }));
  }, []);

  const setTouchedField = useCallback(
    <K extends keyof TValues>(field: K, value: boolean = true): void => {
      setTouched(field, value);
      void validateField(field);
    },
    [setTouched, validateField]
  );

  const handleSubmit = useCallback(
    async (event?: FormEvent): Promise<void> => {
      event?.preventDefault();

      if (!onSubmit) {
        return;
      }

      const valid = validate();
      if (!valid) {
        return;
      }

      setIsSubmitting(true);
      setSubmitError(null);
      setErrors(prev => {
        if (prev._form === undefined) {
          return prev;
        }
        const next: FormErrors<TValues> = { ...prev };
        delete next._form;
        return next;
      });

      try {
        await onSubmit(values);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        setSubmitError(message);
        setErrors(prev => ({ ...prev, _form: message }));
        // Errors are captured in state so we avoid rethrowing to prevent unhandled rejections in callers
        // Note: Logging should be handled by the consuming application
      } finally {
        setIsSubmitting(false);
      }
    },
    [validate, onSubmit, values]
  );

  const reset = useCallback((): void => {
    setValues(initialValues);
    setErrors({});
    setTouchedState({});
    setSubmitError(null);
    setIsSubmitting(false);
  }, [initialValues]);

  const isValid = useMemo(() => Object.values(errors).every(error => !error), [errors]);

  return {
    values,
    errors,
    touched,
    submitError,
    isSubmitting,
    isValid,
    setValue,
    setTouched,
    setTouchedField,
    validate,
    validateField,
    handleSubmit,
    reset,
  };
}
