'use client'

/**
 * mizan Forms - Typed React Form Hooks for Django Server Functions
 *
 * This module provides the core form state management that generated
 * form hooks use. It integrates with mizan server functions for
 * schema fetching, validation, and submission.
 *
 * Users don't use this directly - they use generated typed hooks:
 *
 *   import { useContactForm } from '@/api/forms'
 *   const form = useContactForm()
 *   form.data.email        // typed!
 *   form.set('email', 'x') // typed!
 */

import {
    useState,
    useEffect,
    useRef,
    useCallback,
    useMemo,
} from 'react'
import type { ZodObject, ZodRawShape, ZodError } from 'zod'
import { useMizan } from './context'
import { MizanError } from './errors'

// Forms always use HTTP transport because Django Allauth and other auth
// systems require full HTTP request semantics (session, cookies, CSRF).

// ============================================================================
// Types
// ============================================================================

/** A choice option for select/radio fields */
export interface FieldChoice {
    value: string
    label: string
}

/** Error source for delineating client vs server validation */
export type ErrorSource = 'zod' | 'server'

/** Field validation error */
export interface FieldError {
    message: string
    code?: string | null
    /** Where this error originated - allows custom rendering per source */
    source?: ErrorSource
}

/** Schema for a single form field */
export interface FieldSchema {
    name: string
    label: string
    type: string
    widget: string
    required: boolean
    disabled: boolean
    help_text: string
    initial?: unknown
    max_length?: number | null
    min_length?: number | null
    choices?: FieldChoice[] | null
}

/** Metadata controlling frontend form behavior */
export interface FormMeta {
    refetch_schema_on_validate: boolean
    live_validation: boolean
    live_form_errors: boolean
}

/**
 * Typed form schema.
 *
 * The `fields` object is keyed by field name for type-safe access.
 * The `fieldOrder` array preserves render order.
 */
export interface FormSchema<TData extends Record<string, unknown>> {
    name: string
    title?: string
    subtitle?: string
    submit_label: string
    fields: { [K in keyof TData]: FieldSchema }
    fieldOrder: (keyof TData & string)[]
    meta: FormMeta
}

/**
 * Typed form errors.
 *
 * Field errors are keyed by field name.
 * Form errors are non-field errors (e.g., "Invalid credentials").
 */
export interface FormErrors<TData extends Record<string, unknown>> {
    fields: { [K in keyof TData]?: FieldError[] }
    form: FieldError[]
}

/**
 * Options for form hooks.
 */
export interface FormOptions {
    /**
     * Control live validation behavior:
     * - true: Enable full live validation (shows all errors including form-level)
     * - false: Disable live validation entirely
     * - 'field-only': Validate fields but hide form-level errors (default)
     *
     * @default 'field-only'
     */
    liveValidation?: boolean | 'field-only'

    /**
     * Debounce time in milliseconds for live validation.
     * @default 350
     */
    debounceMs?: number

    /**
     * Whether to refetch the schema on each validation.
     * Useful for forms with dynamic choice fields.
     * @default false (uses schema.meta value)
     */
    refetchSchemaOnValidate?: boolean

    /**
     * When to run server validation (in addition to Zod):
     * - 'on-submit': Only validate on server during submit (default)
     * - 'live': Also run debounced server validation on field touch
     *
     * Use 'live' for forms with server-only validation rules
     * (e.g., uniqueness checks, DB lookups).
     *
     * @default 'on-submit'
     */
    serverValidation?: 'on-submit' | 'live'
}

/**
 * Form submission result.
 */
export type FormSubmitResult<TResult = unknown> =
    | { success: true; data?: TResult }
    | { success: false; errors: FormErrors<Record<string, unknown>> }

/**
 * Formset submission result.
 */
export type FormsetSubmitResult<TData extends Record<string, unknown> = Record<string, unknown>, TResult = unknown> =
    | { success: true; data?: TResult }
    | { success: false; errors: FormsetErrors<TData> }

/**
 * Typed form state returned by form hooks.
 */
export interface MizanFormState<TData extends Record<string, unknown>> {
    /** Current form data - typed by TData */
    data: TData

    /** Form schema with typed field access */
    schema: FormSchema<TData> | null

    /** Current validation errors - typed by TData */
    errors: FormErrors<TData> | null

    /** Set of field names that have been touched */
    touchedFields: Set<keyof TData & string>

    /** True while loading initial schema */
    loading: boolean

    /** True while submitting the form */
    submitting: boolean

    /** True while validating */
    validating: boolean

    /** Get errors for a specific field, optionally filtered by source */
    getFieldErrors: (field: keyof TData, options?: { source?: ErrorSource }) => FieldError[]

    /** Get form-level errors (non-field errors) */
    getFormErrors: () => FieldError[]

    /** Set a field value - typed! */
    set: <K extends keyof TData>(field: K, value: TData[K]) => void

    /** Mark a field as touched (triggers validation) */
    touch: (field: keyof TData & string) => void

    /** Submit the form */
    submit: () => Promise<FormSubmitResult>

    /** Reset form to initial state */
    reset: () => void

    /** True if there are any validation errors */
    hasErrors: boolean

    /** True if validation passed (no errors) */
    isValid: boolean
}

// ============================================================================
// Internal Types
// ============================================================================

/** Raw schema from backend (fields as array) */
interface RawFormSchema {
    name: string
    title?: string
    subtitle?: string
    submit_label: string
    fields: FieldSchema[]
    meta: FormMeta
}

/** Raw validation response from backend */
interface RawFormValidation {
    errors: Array<{
        field: string
        errors: FieldError[]
    }>
}

/** Form submit success response */
interface FormSubmitPassResponse {
    success: true
    data?: unknown
}

/** Form submit failure response */
interface FormSubmitFailResponse {
    success: false
    errors: RawFormValidation
}

// ============================================================================
// Transforms
// ============================================================================

/**
 * Transform raw schema (fields array) to typed schema (fields object).
 */
function transformSchema<TData extends Record<string, unknown>>(
    raw: RawFormSchema
): FormSchema<TData> {
    const fields = {} as { [K in keyof TData]: FieldSchema }
    const fieldOrder: (keyof TData & string)[] = []

    for (const field of raw.fields) {
        const name = field.name as keyof TData & string
        fields[name] = field
        fieldOrder.push(name)
    }

    return {
        name: raw.name,
        title: raw.title,
        subtitle: raw.subtitle,
        submit_label: raw.submit_label,
        fields,
        fieldOrder,
        meta: raw.meta,
    }
}

/**
 * Transform raw validation (errors array) to typed errors (fields object).
 */
function transformValidation<TData extends Record<string, unknown>>(
    raw: RawFormValidation
): FormErrors<TData> {
    const fields = {} as { [K in keyof TData]?: FieldError[] }
    const form: FieldError[] = []

    for (const fieldErrors of raw.errors) {
        if (fieldErrors.field === '__all__') {
            // Tag form-level errors with server source
            form.push(...fieldErrors.errors.map(e => ({ ...e, source: 'server' as const })))
        } else {
            const name = fieldErrors.field as keyof TData
            // Tag field errors with server source
            fields[name] = fieldErrors.errors.map(e => ({ ...e, source: 'server' as const }))
        }
    }

    return { fields, form }
}

/**
 * Initialize form data from schema with initial values.
 */
function initializeData<TData extends Record<string, unknown>>(
    schema: FormSchema<TData>
): TData {
    const data = {} as TData

    for (const name of schema.fieldOrder) {
        const field = schema.fields[name]
        ;(data as Record<string, unknown>)[name] = field.initial ?? ''
    }

    return data
}

/**
 * Clean invalid choice values when schema changes.
 */
function cleanInvalidChoices<TData extends Record<string, unknown>>(
    data: TData,
    schema: FormSchema<TData>
): TData {
    const result = { ...data }

    for (const name of schema.fieldOrder) {
        const field = schema.fields[name]
        if (field.choices) {
            const validValues = field.choices.map(c => String(c.value))
            if (!validValues.includes(String(data[name]))) {
                ;(result as Record<string, unknown>)[name] = ''
            }
        }
    }

    return result
}

// ============================================================================
// Formset Types
// ============================================================================

/**
 * Typed formset schema.
 */
export interface FormsetSchema<TData extends Record<string, unknown>> {
    forms: FormSchema<TData>[]
    min_num: number
    max_num: number
    can_delete: boolean
    can_order: boolean
}

/**
 * Typed formset errors.
 */
export interface FormsetErrors<TData extends Record<string, unknown>> {
    forms: FormErrors<TData>[]
    nonForm: FieldError[]
}

/**
 * Typed formset state returned by formset hooks.
 */
export interface MizanFormsetState<TData extends Record<string, unknown>> {
    /** Array of form data objects - each typed by TData */
    forms: TData[]

    /** Array of form schemas */
    schemas: FormSchema<TData>[]

    /** Current validation errors */
    errors: FormsetErrors<TData> | null

    /** True while loading initial schema */
    loading: boolean

    /** True while submitting */
    submitting: boolean

    /** Minimum number of forms required */
    minNum: number

    /** Maximum number of forms allowed */
    maxNum: number

    /** Whether forms can be deleted */
    canDelete: boolean

    /** Whether forms can be reordered */
    canOrder: boolean

    /** Whether a new form can be added */
    canAddForm: boolean

    /** Whether a form can be removed */
    canRemoveForm: boolean

    /** Get a field value - typed! */
    get: <K extends keyof TData>(formIndex: number, field: K) => TData[K] | undefined

    /** Set a field value - typed! */
    set: <K extends keyof TData>(formIndex: number, field: K, value: TData[K]) => void

    /** Get field schema */
    field: (formIndex: number, field: keyof TData) => FieldSchema | undefined

    /** Mark a field as touched (triggers validation) */
    touch: (formIndex: number, field: keyof TData & string) => void

    /** Get errors for a specific field */
    getFieldErrors: (formIndex: number, field: keyof TData) => FieldError[]

    /** Add a new form */
    addForm: (data?: Partial<TData>) => void

    /** Remove a form by index */
    removeForm: (index: number) => void

    /** Submit all forms */
    submit: () => Promise<FormsetSubmitResult<TData>>

    /** Reset to initial state */
    reset: () => void
}

// ============================================================================
// Core Hook
// ============================================================================

/**
 * Configuration for useMizanFormCore.
 * This is used by generated hooks - not directly by users.
 */
export interface FormCoreConfig<TData extends Record<string, unknown>> {
    /** Form name (used for server function calls: name.schema, name.validate, name.submit) */
    name: string

    /**
     * Zod schema for client-side validation.
     * Generated from Django form field definitions.
     * Provides instant validation feedback without server round-trip.
     */
    zodSchema?: ZodObject<ZodRawShape>

    /** Form options */
    options?: FormOptions
}

/**
 * Core form hook that generated hooks use internally.
 *
 * This is NOT meant to be used directly by users.
 * Use generated typed hooks instead (e.g., useContactForm).
 *
 * @internal
 */
export function useMizanFormCore<TData extends Record<string, unknown>>(
    config: FormCoreConfig<TData>
): MizanFormState<TData> {
    const { name, zodSchema, options = {} } = config
    const {
        liveValidation: liveValidationOption = 'field-only',
        debounceMs = 350,
        refetchSchemaOnValidate: refetchOption,
        serverValidation: serverValidationMode = 'on-submit',
    } = options

    const { call } = useMizan()

    // State
    const [data, setData] = useState<TData>({} as TData)
    const [schema, setSchema] = useState<FormSchema<TData> | null>(null)
    const [loading, setLoading] = useState(true)
    const [submitting, setSubmitting] = useState(false)
    const [validating, setValidating] = useState(false)
    const [touchedFields, setTouchedFields] = useState<Set<keyof TData & string>>(new Set())
    const [errors, setErrors] = useState<FormErrors<TData> | null>(null)
    const [pendingFields, setPendingFields] = useState<Set<keyof TData & string>>(new Set())

    // Refs for validation tracking
    const validationSeqRef = useRef(0)
    const touchTimeoutsRef = useRef<Map<string, number>>(new Map())

    // Zod validation (instant client-side)
    const validateWithZod = useCallback((dataToValidate: TData): FormErrors<TData> | null => {
        if (!zodSchema) return null

        const result = zodSchema.safeParse(dataToValidate)
        if (result.success) {
            return { fields: {} as { [K in keyof TData]?: FieldError[] }, form: [] }
        }

        // Convert Zod errors to our format with source tag
        const fields = {} as { [K in keyof TData]?: FieldError[] }
        for (const issue of result.error.issues) {
            const fieldName = issue.path[0] as keyof TData
            if (!fields[fieldName]) {
                fields[fieldName] = []
            }
            fields[fieldName]!.push({
                message: issue.message,
                code: issue.code,
                source: 'zod',
            })
        }

        return { fields, form: [] }
    }, [zodSchema])

    // Effective settings
    const shouldLiveValidate = useCallback(() => {
        if (liveValidationOption === false) return false
        if (liveValidationOption === true || liveValidationOption === 'field-only') return true
        return schema?.meta?.live_validation ?? true
    }, [liveValidationOption, schema?.meta?.live_validation])

    const shouldShowFormErrors = useCallback(() => {
        if (liveValidationOption === 'field-only') return false
        if (liveValidationOption === false) return false
        if (liveValidationOption === true) return true
        return schema?.meta?.live_form_errors ?? false
    }, [liveValidationOption, schema?.meta?.live_form_errors])

    const shouldRefetchSchema = useCallback(() => {
        if (refetchOption !== undefined) return refetchOption
        return schema?.meta?.refetch_schema_on_validate ?? false
    }, [refetchOption, schema?.meta?.refetch_schema_on_validate])

    // Load initial schema
    useEffect(() => {
        let cancelled = false
        setLoading(true)

        const load = async () => {
            try {
                const rawSchema = await call<{ data?: Record<string, unknown> }, RawFormSchema>(
                    `${name}.schema`,
                    { data: {} },
                    'http'  // Forms always use HTTP
                )

                if (cancelled) return

                const typedSchema = transformSchema<TData>(rawSchema)
                setSchema(typedSchema)
                setData(initializeData(typedSchema))
                setTouchedFields(new Set())
                setErrors(null)
            } catch (err) {
                console.error('Form schema load failed:', err)
            } finally {
                if (!cancelled) {
                    setLoading(false)
                }
            }
        }

        load()

        return () => {
            cancelled = true
        }
    }, [name, call])

    // Cleanup timeouts on unmount
    useEffect(() => {
        return () => {
            touchTimeoutsRef.current.forEach(id => clearTimeout(id))
        }
    }, [])

    // Trigger validation
    // Strategy: Zod first (instant), then optionally server (for complex rules)
    const triggerValidation = useCallback(async (zodOnly: boolean = false) => {
        if (!schema) return

        const seq = ++validationSeqRef.current

        // Step 1: Instant Zod validation
        const zodErrors = validateWithZod(data)
        if (zodErrors) {
            setErrors(zodErrors)
            setPendingFields(new Set())

            // If zodOnly mode, stop here (instant feedback only)
            if (zodOnly) {
                return
            }
            // If Zod found errors, skip server validation - no point
            // Server validation is for catching server-only rules (uniqueness, DB lookups)
            // when client-side validation passes
            if (Object.keys(zodErrors.fields).length > 0) {
                return
            }
        }

        // Step 2: Server validation (for complex rules like uniqueness, DB lookups)
        setValidating(true)

        try {
            let currentSchema = schema
            let currentData = data

            // Refetch schema if needed
            if (shouldRefetchSchema()) {
                const rawSchema = await call<{ data: TData }, RawFormSchema>(
                    `${name}.schema`,
                    { data },
                    'http'  // Forms always use HTTP
                )

                if (seq !== validationSeqRef.current) return

                currentSchema = transformSchema<TData>(rawSchema)
                setSchema(currentSchema)

                // Clean invalid choice values
                currentData = cleanInvalidChoices(data, currentSchema)
                if (JSON.stringify(currentData) !== JSON.stringify(data)) {
                    setData(currentData)
                }
            }

            // Validate with server
            const rawValidation = await call<{ data: TData }, RawFormValidation>(
                `${name}.validate`,
                { data: currentData },
                'http'  // Forms always use HTTP
            )

            if (seq !== validationSeqRef.current) return

            let serverErrors = transformValidation<TData>(rawValidation)

            // Filter out form-level errors if not showing them
            if (!shouldShowFormErrors()) {
                serverErrors = { ...serverErrors, form: [] }
            }

            // In hybrid mode, merge Zod and server errors
            // Server errors take precedence per-field (more authoritative)
            if (zodErrors && serverValidationMode === 'live') {
                const mergedFields = { ...zodErrors.fields } as { [K in keyof TData]?: FieldError[] }

                // Server errors override Zod errors per-field
                for (const [field, errors] of Object.entries(serverErrors.fields)) {
                    if (errors && errors.length > 0) {
                        mergedFields[field as keyof TData] = errors
                    }
                }

                setErrors({
                    fields: mergedFields,
                    form: serverErrors.form,
                })
            } else {
                setErrors(serverErrors)
            }

            setPendingFields(new Set())
        } catch (err) {
            console.debug('Validation error:', err)
            // On server error, keep Zod errors if any
        } finally {
            if (seq === validationSeqRef.current) {
                setValidating(false)
            }
        }
    }, [schema, data, name, call, shouldRefetchSchema, shouldShowFormErrors, validateWithZod, serverValidationMode])

    // Set field value
    const set = useCallback(<K extends keyof TData>(field: K, value: TData[K]) => {
        setData(prev => ({ ...prev, [field]: value }))
    }, [])

    // Touch field (triggers validation)
    const touch = useCallback((field: keyof TData & string) => {
        setTouchedFields(prev => new Set(prev).add(field))

        if (!shouldLiveValidate()) return

        // Instant Zod validation (no debounce needed)
        if (zodSchema) {
            triggerValidation(true) // zodOnly = true - instant feedback

            // If hybrid mode enabled, also schedule debounced server validation
            if (serverValidationMode === 'live') {
                const timeouts = touchTimeoutsRef.current
                const existing = timeouts.get(field)
                if (existing) clearTimeout(existing)

                const id = window.setTimeout(() => {
                    timeouts.delete(field)
                    triggerValidation(false) // Full validation including server
                }, debounceMs)

                timeouts.set(field, id)
            }
            return
        }

        // Server-only validation (no Zod schema) - use pending state
        setPendingFields(prev => new Set(prev).add(field))

        const timeouts = touchTimeoutsRef.current
        const existing = timeouts.get(field)
        if (existing) {
            clearTimeout(existing)
        }

        const id = window.setTimeout(() => {
            timeouts.delete(field)
            triggerValidation(false) // Full validation including server
        }, debounceMs)

        timeouts.set(field, id)
    }, [shouldLiveValidate, triggerValidation, debounceMs, zodSchema, serverValidationMode])

    // Get field errors (only for touched fields, optionally filtered by source)
    const getFieldErrors = useCallback((
        field: keyof TData,
        options?: { source?: ErrorSource }
    ): FieldError[] => {
        // Don't show errors for fields that haven't been touched yet
        if (!touchedFields.has(field as keyof TData & string)) {
            return []
        }
        // Don't show errors for fields with pending server validation
        if (pendingFields.has(field as keyof TData & string)) {
            return []
        }
        const fieldErrors = errors?.fields[field] ?? []

        // Filter by source if requested
        if (options?.source) {
            return fieldErrors.filter(e => e.source === options.source)
        }

        return fieldErrors
    }, [errors, pendingFields, touchedFields])

    // Get form errors
    const getFormErrors = useCallback((): FieldError[] => {
        return errors?.form ?? []
    }, [errors])

    // Reset form
    const reset = useCallback(() => {
        if (schema) {
            setData(initializeData(schema))
        }
        setTouchedFields(new Set())
        setPendingFields(new Set())
        setErrors(null)
        validationSeqRef.current++
    }, [schema])

    // Submit form
    const submit = useCallback(async (): Promise<FormSubmitResult> => {
        setSubmitting(true)
        validationSeqRef.current++

        try {
            const response = await call<TData, FormSubmitPassResponse | FormSubmitFailResponse>(
                `${name}.submit`,
                data,
                'http'  // Forms always use HTTP
            )

            if (response.success) {
                return { success: true, data: response.data }
            } else {
                const typedErrors = transformValidation<TData>(response.errors)
                setErrors(typedErrors)
                return { success: false, errors: typedErrors }
            }
        } catch (err) {
            // Handle MizanError with validation details
            if (err instanceof MizanError && err.isValidationError()) {
                const rawFieldErrors = err.getFieldErrors()
                const fields = {} as { [K in keyof TData]?: FieldError[] }

                if (rawFieldErrors) {
                    for (const [fieldName, messages] of Object.entries(rawFieldErrors)) {
                        fields[fieldName as keyof TData] = messages.map(msg => ({ message: msg }))
                    }
                }

                const typedErrors: FormErrors<TData> = { fields, form: [] }
                setErrors(typedErrors)
                return { success: false, errors: typedErrors }
            }

            console.error('Form submission error:', err)
            return {
                success: false,
                errors: {
                    fields: {} as { [K in keyof TData]?: FieldError[] },
                    form: [{ message: 'An error occurred during submission' }],
                },
            }
        } finally {
            setSubmitting(false)
        }
    }, [name, data, call])

    // Computed properties
    // Only consider errors for touched fields (consistent with getFieldErrors)
    const hasErrors = useMemo(() => {
        if (!errors) return false
        // Check for field errors only in touched fields
        const hasFieldErrors = Array.from(touchedFields).some(field => {
            const fieldErrors = errors.fields[field as keyof TData]
            return fieldErrors && fieldErrors.length > 0
        })
        const hasFormErrors = errors.form.length > 0
        return hasFieldErrors || hasFormErrors
    }, [errors, touchedFields])

    const isValid = useMemo(() => {
        if (errors === null || hasErrors) return false
        // Also check that all required fields have been touched
        if (schema) {
            for (const fieldName of schema.fieldOrder) {
                const field = schema.fields[fieldName]
                if (field.required && !touchedFields.has(fieldName)) return false
            }
        }
        return true
    }, [errors, hasErrors, schema, touchedFields])

    return {
        data,
        schema,
        errors,
        touchedFields,
        loading,
        submitting,
        validating,
        getFieldErrors,
        getFormErrors,
        set,
        touch,
        submit,
        reset,
        hasErrors,
        isValid,
    }
}

// ============================================================================
// Formset Core Hook
// ============================================================================

/** Raw formset schema from backend */
interface RawFormsetSchema {
    forms: RawFormSchema[]
    min_num: number
    max_num: number
    can_delete: boolean
    can_order: boolean
}

/** Raw formset validation from backend */
interface RawFormsetValidation {
    general: string[]
    per_form: RawFormValidation[]
}

/**
 * Transform raw formset validation to typed errors.
 */
function transformFormsetValidation<TData extends Record<string, unknown>>(
    raw: RawFormsetValidation
): FormsetErrors<TData> {
    return {
        forms: raw.per_form.map(v => transformValidation<TData>(v)),
        nonForm: raw.general.map(msg => ({ message: msg })),
    }
}

/**
 * Configuration for useMizanFormsetCore.
 */
export interface FormsetCoreConfig<TData extends Record<string, unknown>> {
    /** Form name (used for server function calls) */
    name: string

    /**
     * Zod schema for client-side validation of individual forms.
     * Generated from Django form field definitions.
     */
    zodSchema?: ZodObject<ZodRawShape>

    /** Initial number of forms */
    initialCount?: number

    /** Whether to enable live validation */
    liveValidation?: boolean

    /** Debounce time for validation */
    debounceMs?: number
}

/**
 * Core formset hook that generated hooks use internally.
 *
 * @internal
 */
export function useMizanFormsetCore<TData extends Record<string, unknown>>(
    config: FormsetCoreConfig<TData>
): MizanFormsetState<TData> {
    const {
        name,
        initialCount = 1,
        liveValidation = true,
        debounceMs = 350,
    } = config

    const { call } = useMizan()

    // State
    const [forms, setForms] = useState<TData[]>(
        Array(initialCount).fill(null).map(() => ({} as TData))
    )
    const [schemas, setSchemas] = useState<FormSchema<TData>[]>([])
    const [loading, setLoading] = useState(true)
    const [submitting, setSubmitting] = useState(false)
    const [errors, setErrors] = useState<FormsetErrors<TData> | null>(null)
    const [touchedFields, setTouchedFields] = useState<Set<string>>(new Set())

    const [minNum, setMinNum] = useState(0)
    const [maxNum, setMaxNum] = useState(1000)
    const [canDelete, setCanDelete] = useState(false)
    const [canOrder, setCanOrder] = useState(false)

    // Refs
    const touchTimeoutsRef = useRef<Map<string, number>>(new Map())
    const validationSeqRef = useRef(0)

    // Fetch schemas
    const updateSchema = useCallback(async (formsData: TData[]) => {
        try {
            const rawSchema = await call<{ forms: TData[] }, RawFormsetSchema>(
                `${name}.formset.schema`,
                { forms: formsData },
                'http'  // Forms always use HTTP
            )

            setSchemas(rawSchema.forms.map(raw => transformSchema<TData>(raw)))
            setMinNum(rawSchema.min_num)
            setMaxNum(rawSchema.max_num)
            setCanDelete(rawSchema.can_delete)
            setCanOrder(rawSchema.can_order)
        } catch (err) {
            console.error('Formset schema load failed:', err)
        }
    }, [name, call])

    // Initial load
    useEffect(() => {
        let cancelled = false

        const load = async () => {
            setLoading(true)
            await updateSchema(forms)
            if (!cancelled) {
                setLoading(false)
            }
        }

        load()

        return () => {
            cancelled = true
        }
    }, []) // Only on mount

    // Update schema when form count changes
    useEffect(() => {
        if (!loading) {
            updateSchema(forms)
        }
    }, [forms.length])

    // Cleanup timeouts
    useEffect(() => {
        return () => {
            touchTimeoutsRef.current.forEach(id => clearTimeout(id))
        }
    }, [])

    // Validate
    const triggerValidation = useCallback(async () => {
        if (!liveValidation) return

        const seq = ++validationSeqRef.current

        try {
            const rawValidation = await call<{ forms: TData[] }, RawFormsetValidation>(
                `${name}.formset.validate`,
                { forms },
                'http'  // Forms always use HTTP
            )

            if (seq !== validationSeqRef.current) return

            setErrors(transformFormsetValidation<TData>(rawValidation))
        } catch (err) {
            console.debug('Formset validation error:', err)
        }
    }, [name, forms, call, liveValidation])

    // Get field value
    const get = useCallback(<K extends keyof TData>(
        formIndex: number,
        field: K
    ): TData[K] | undefined => {
        return forms[formIndex]?.[field]
    }, [forms])

    // Set field value
    const set = useCallback(<K extends keyof TData>(
        formIndex: number,
        field: K,
        value: TData[K]
    ) => {
        setForms(prev => {
            const updated = [...prev]
            updated[formIndex] = { ...updated[formIndex], [field]: value }
            return updated
        })

        // Also touch the field
        const key = `${formIndex}-${String(field)}`
        setTouchedFields(prev => new Set(prev).add(key))

        // Debounced validation
        const timeouts = touchTimeoutsRef.current
        const existing = timeouts.get(key)
        if (existing) clearTimeout(existing)

        const id = window.setTimeout(() => {
            timeouts.delete(key)
            triggerValidation()
        }, debounceMs)

        timeouts.set(key, id)
    }, [triggerValidation, debounceMs])

    // Get field schema
    const field = useCallback((
        formIndex: number,
        fieldName: keyof TData
    ): FieldSchema | undefined => {
        return schemas[formIndex]?.fields[fieldName]
    }, [schemas])

    // Touch field
    const touch = useCallback((formIndex: number, fieldName: keyof TData & string) => {
        const key = `${formIndex}-${fieldName}`
        setTouchedFields(prev => new Set(prev).add(key))

        const timeouts = touchTimeoutsRef.current
        const existing = timeouts.get(key)
        if (existing) clearTimeout(existing)

        const id = window.setTimeout(() => {
            timeouts.delete(key)
            triggerValidation()
        }, debounceMs)

        timeouts.set(key, id)
    }, [triggerValidation, debounceMs])

    // Get field errors
    const getFieldErrors = useCallback((
        formIndex: number,
        field: keyof TData
    ): FieldError[] => {
        return errors?.forms[formIndex]?.fields[field] ?? []
    }, [errors])

    // Add form
    const addForm = useCallback((data: Partial<TData> = {}) => {
        setForms(prev => {
            if (prev.length >= maxNum) return prev
            return [...prev, data as TData]
        })
    }, [maxNum])

    // Remove form
    const removeForm = useCallback((index: number) => {
        setForms(prev => {
            if (prev.length <= minNum) return prev
            return prev.filter((_, i) => i !== index)
        })
    }, [minNum])

    // Submit
    const submit = useCallback(async (): Promise<FormsetSubmitResult<TData>> => {
        setSubmitting(true)
        validationSeqRef.current++

        try {
            // Check for files
            const hasFiles = forms.some(form =>
                Object.values(form).some(value => value instanceof File)
            )

            if (hasFiles) {
                // Use HTTP with FormData for file uploads
                // Server functions don't support multipart yet
                throw new Error('File uploads in formsets require HTTP transport (not yet implemented)')
            }

            const response = await call<
                { forms: TData[] },
                { success: true } | { success: false; errors: RawFormsetValidation }
            >(
                `${name}.formset.submit`,
                { forms },
                'http'  // Forms always use HTTP
            )

            if (response.success) {
                return { success: true }
            } else {
                const typedErrors = transformFormsetValidation<TData>(response.errors)
                setErrors(typedErrors)
                return { success: false, errors: typedErrors }
            }
        } catch (err) {
            console.error('Formset submission error:', err)
            const errorMessage = err instanceof Error ? err.message : 'Submission failed'
            return {
                success: false,
                errors: {
                    forms: [],
                    nonForm: [{ message: errorMessage }],
                },
            }
        } finally {
            setSubmitting(false)
        }
    }, [name, forms, call])

    // Reset
    const reset = useCallback(() => {
        setForms(Array(initialCount).fill(null).map(() => ({} as TData)))
        setTouchedFields(new Set())
        setErrors(null)
        validationSeqRef.current++
    }, [initialCount])

    return {
        forms,
        schemas,
        errors,
        loading,
        submitting,
        minNum,
        maxNum,
        canDelete,
        canOrder,
        canAddForm: forms.length < maxNum,
        canRemoveForm: forms.length > minNum,
        get,
        set,
        field,
        touch,
        getFieldErrors,
        addForm,
        removeForm,
        submit,
        reset,
    }
}
