import { computed, reactive, ref, type Ref } from 'vue'
import { useFormErrors } from '~/shared/composables/useFormErrors'
import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api'
import { removeCollectionRow } from '~/shared/utils/collectionRow'
import {
    emptyCarrierAddress,
    emptyCarrierAddressCopy,
    emptyCarrierContact,
    emptyCarrierMain,
    emptyCarrierPrice,
    type CarrierAddressCopy,
    type CarrierAddressFormDraft,
    type CarrierContactFormDraft,
    type CarrierMainDraft,
    type CarrierMainResponse,
    type CarrierPriceFormDraft,
} from '~/modules/transport/types/carrierForm'
import { buildCarrierAddressPayload, isCarrierAddressValid } from '~/modules/transport/utils/forms/carrierAddress'
import { buildCarrierContactPayload, isCarrierContactBlank, isCarrierContactNamed } from '~/modules/transport/utils/forms/carrierContact'
import { buildCarrierPricePayload, isCarrierPriceValid } from '~/modules/transport/utils/forms/carrierPrice'
import {
    mapAddressToDraft,
    mapContactToDraft,
    mapMainToDraft,
    mapPriceToDraft,
    type CarrierDetail,
} from '~/modules/transport/utils/forms/carrierMappers'
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'

/** Nom du cas spécial « compte-propre » LIOT (comparaison insensible à la casse, RG-4.01). */
const LIOT_NAME = 'LIOT'

/**
 * Workflow de l'écran « Ajouter un transporteur » (M4 Transport, ERP-165) —
 * miroir conceptuel de `useSupplierForm` (M2) / `useProviderForm` (M3).
 *
 * Périmètre ERP-165 : le formulaire PRINCIPAL (pré-onglets) et l'orchestration de
 * la barre d'onglets. La création est INCRÉMENTALE (spec-front § Écran Ajouter) :
 *  - on POST d'abord le formulaire principal (`POST /api/carriers`) ;
 *  - au succès le bloc principal passe en lecture seule, le 1er onglet (Qualimat)
 *    se déverrouille et devient actif ;
 *  - chaque onglet validé déverrouille le suivant (PATCH partiels par groupe de
 *    sérialisation) et passe en lecture seule.
 *
 * Les champs conditionnels du formulaire principal (indexation / benne / volume
 * si affrété, décharge si AUTRE, cas LIOT) et la saisie assistée QUALIMAT arrivent
 * à ERP-166 ; le contenu des onglets Adresses / Contacts / Prix aux tickets
 * suivants. Ce composable pose le POST principal, le PATCH partiel et le gating
 * des onglets.
 *
 * État 100 % local à l'instance (refs / reactive) — aucune persistance URL.
 */

/**
 * Clés des onglets du flux de création, dans l'ordre de la barre (spec-front
 * § Écran Ajouter). Toujours les 4 (pas de gating par permission au M4, ≠ l'onglet
 * Comptabilité du M3).
 */
export const CARRIER_TAB_KEYS = ['qualimat', 'addresses', 'contacts', 'prices'] as const

export function useCarrierForm() {
    const api = useApi()
    const { t } = useI18n()
    const toast = useToast()

    // Erreurs de validation par champ (ERP-101) du formulaire principal.
    const mainErrors = useFormErrors()

    // ── État du transporteur créé ─────────────────────────────────────────────
    const carrierId = ref<number | null>(null)
    const mainLocked = ref(false)
    const mainSubmitting = ref(false)
    const tabSubmitting = ref(false)

    // ── Formulaire principal ──────────────────────────────────────────────────
    const main = reactive<CarrierMainDraft>(emptyCarrierMain())

    // Adresse copiée depuis QUALIMAT à la sélection (alimente l'onglet Adresses,
    // ticket ultérieur). Vide tant qu'aucun transporteur QUALIMAT n'est intégré.
    const qualimatAddress = ref<CarrierAddressCopy>(emptyCarrierAddressCopy())

    // ── Affichage conditionnel du formulaire principal (RG-4.01 / 4.02 / 4.03) ──
    // Cas LIOT : nom == « LIOT » → seul `liotPlates` est pertinent, le reste masqué.
    const isLiot = computed(() => main.name.trim().toUpperCase() === LIOT_NAME)
    // Transporteur QUALIMAT : la FK est posée → certification figée à « QUALIMAT ».
    const isQualimat = computed(() => main.qualimatCarrierIri !== null)
    // Certification masquée en cas LIOT ; lecture seule si QUALIMAT (ou bloc verrouillé).
    const showCertification = computed(() => !isLiot.value)
    const certificationReadonly = computed(() => isQualimat.value || mainLocked.value)
    // RG-4.03 : champs d'affrètement (indexation / contenant / volume) visibles et
    // obligatoires si « Affréter » coché — masqués en cas LIOT.
    const showCharteredFields = computed(() => main.isChartered && !isLiot.value)
    // RG-4.02 : décharge visible et obligatoire si certification == AUTRE (hors LIOT).
    const showDischarge = computed(() => main.certificationType === 'AUTRE' && !isLiot.value)

    // ── Onglets : ordre + gating progressif ───────────────────────────────────
    const tabKeys = ref<string[]>([...CARRIER_TAB_KEYS])
    // Index du dernier onglet déverrouillé. L'onglet Qualimat (index 0) est la saisie
    // assistée du formulaire principal : accessible DÈS LE DÉPART (≠ Adresses /
    // Contacts / Prix, déverrouillés seulement après le POST principal).
    const unlockedIndex = ref(0)
    const activeTab = ref<string>(CARRIER_TAB_KEYS[0])
    // Onglets validés (passent en lecture seule).
    const validated = reactive<Record<string, boolean>>({})
    // Mode MODIFICATION (ticket ultérieur) : navigation libre, pas de verrouillage
    // ni de bascule automatique d'onglet à la validation (cf. completeTab).
    const editMode = ref(false)

    function isValidated(key: string): boolean {
        return validated[key] === true
    }

    function tabIndex(key: string): number {
        return tabKeys.value.indexOf(key)
    }

    /**
     * Validation FRONT du formulaire principal : seul le nom est requis côté front
     * (ERP-101) : feedback immédiat sur tous les champs obligatoires (y compris
     * conditionnels), alignés sur les RG du back (qui reste autoritaire) :
     *  - RG-4.01 : nom requis ; certification requise hors cas LIOT (où tout est masqué) ;
     *  - RG-4.02 : décharge requise si certification AUTRE ;
     *  - RG-4.03 : indexation + contenant + volume requis si « Affréter ».
     */
    function validateMainFront(): boolean {
        let valid = true
        if (!main.name?.trim()) {
            mainErrors.setError('name', t('transport.carriers.form.errors.nameRequired'))
            valid = false
        }

        // Cas LIOT : seul le nom compte, les autres champs sont masqués (RG-4.01).
        if (isLiot.value) {
            return valid
        }

        // RG-4.01 : certification obligatoire hors LIOT.
        if (!main.certificationType) {
            mainErrors.setError('certificationType', t('transport.carriers.form.errors.certificationRequired'))
            valid = false
        }

        // RG-4.02 : décharge obligatoire si certification AUTRE.
        if (main.certificationType === 'AUTRE' && !main.dischargeDocumentIri) {
            mainErrors.setError('dischargeDocument', t('transport.carriers.form.errors.dischargeRequired'))
            valid = false
        }

        // RG-4.03 : indexation / contenant / volume obligatoires si affrété.
        if (main.isChartered) {
            if (!main.indexationRate.trim()) {
                mainErrors.setError('indexationRate', t('transport.carriers.form.errors.indexationRequired'))
                valid = false
            }
            if (!main.containerType) {
                mainErrors.setError('containerType', t('transport.carriers.form.errors.containerTypeRequired'))
                valid = false
            }
            if (!main.volumeM3.trim()) {
                mainErrors.setError('volumeM3', t('transport.carriers.form.errors.volumeRequired'))
                valid = false
            }
        }

        return valid
    }

    /**
     * Payload du POST principal (groupe `carrier:write:main`). `name` et
     * `certificationType` sont omis s'ils sont vides afin que la 422 porte la
     * violation métier (NotBlank sur le nom, « certification obligatoire » sur la
     * certification) sur le champ plutôt qu'une erreur de type.
     */
    function buildMainPayload(): Record<string, unknown> {
        // Cas LIOT (RG-4.01) : seul `liotPlates` est pertinent ; les autres champs
        // sont masqués côté front et non envoyés (le back stocke ce qu'il reçoit).
        if (isLiot.value) {
            const payload: Record<string, unknown> = { name: main.name, isChartered: false }
            if (main.liotPlates.trim()) {
                payload.liotPlates = main.liotPlates
            }
            return payload
        }

        const payload: Record<string, unknown> = { isChartered: main.isChartered }
        if (main.name.trim()) {
            payload.name = main.name
        }
        if (main.certificationType) {
            payload.certificationType = main.certificationType
        }
        // FK QUALIMAT (saisie assistée, § 2.5) envoyée si une ligne a été intégrée.
        if (main.qualimatCarrierIri) {
            payload.qualimatCarrier = main.qualimatCarrierIri
        }
        // RG-4.02 : décharge envoyée seulement en certification AUTRE ; omise quand
        // absente pour que la 422 « obligatoire » porte sur le champ.
        if (main.certificationType === 'AUTRE' && main.dischargeDocumentIri) {
            payload.dischargeDocument = main.dischargeDocumentIri
        }
        // RG-4.03 : indexation / contenant / volume envoyés seulement si affrété ;
        // omis quand vides pour déclencher la 422 NotBlank inline sur le champ.
        if (main.isChartered) {
            if (main.indexationRate.trim()) {
                payload.indexationRate = main.indexationRate
            }
            if (main.containerType) {
                payload.containerType = main.containerType
            }
            if (main.volumeM3.trim()) {
                payload.volumeM3 = main.volumeM3
            }
        }
        return payload
    }

    /**
     * POST /carriers (groupe `carrier:write:main`). Pré-check front, puis création.
     * Au succès : verrouille le bloc principal, déverrouille l'onglet Adresses et
     * bascule dessus (l'onglet Qualimat, saisie assistée, était déjà accessible).
     * Retourne true si créé, false sinon.
     */
    async function submitMain(): Promise<boolean> {
        if (mainSubmitting.value) return false
        mainErrors.clearErrors()
        if (!validateMainFront()) return false

        mainSubmitting.value = true
        try {
            const created = await api.post<CarrierMainResponse>('/carriers', buildMainPayload(), {
                headers: { Accept: 'application/ld+json' },
                toast: false,
            })

            carrierId.value = created.id
            // Réaffiche les valeurs normalisées renvoyées par le serveur (nom en
            // UPPERCASE — RG-4.13 ; certification éventuellement forcée).
            main.name = created.name ?? main.name
            main.certificationType = created.certificationType ?? main.certificationType

            mainLocked.value = true
            // Déverrouille l'onglet suivant (Adresses, index 1) et bascule dessus.
            unlockedIndex.value = Math.max(unlockedIndex.value, 1)
            activeTab.value = tabKeys.value[1] ?? CARRIER_TAB_KEYS[1]
            toast.success({ title: t('transport.carriers.toast.createSuccess') })
            return true
        }
        catch (error) {
            // 409 = doublon de nom (RG-4.12) → erreur inline dédiée + toast ;
            // 422 → mapping inline par champ ; autre → toast de fallback (ERP-101).
            const status = (error as { response?: { status?: number } })?.response?.status
            if (status === 409) {
                const message = t('transport.carriers.form.duplicateName')
                mainErrors.setError('name', message)
                toast.error({ title: t('transport.carriers.toast.error'), message })
            }
            else {
                mainErrors.handleApiError(error, { fallbackMessage: t('transport.carriers.toast.error') })
            }
            return false
        }
        finally {
            mainSubmitting.value = false
        }
    }

    /**
     * MODIFICATION du formulaire principal (ERP-170) : PATCH /api/carriers/{id} sur le
     * groupe carrier:write:main (PAS de re-POST). Pré-check front + 409 doublon / 422
     * inline comme `submitMain`. Ne verrouille rien et ne bascule pas d'onglet (édition
     * = navigation libre). Retourne true si le PATCH a réussi.
     */
    async function updateMain(): Promise<boolean> {
        if (carrierId.value === null || mainSubmitting.value) return false
        mainErrors.clearErrors()
        if (!validateMainFront()) return false

        mainSubmitting.value = true
        try {
            const updated = await api.patch<CarrierMainResponse>(
                `/carriers/${carrierId.value}`,
                buildMainPayload(),
                { toast: false },
            )
            main.name = updated.name ?? main.name
            main.certificationType = updated.certificationType ?? main.certificationType
            return true
        }
        catch (error) {
            const status = (error as { response?: { status?: number } })?.response?.status
            if (status === 409) {
                const message = t('transport.carriers.form.duplicateName')
                mainErrors.setError('name', message)
                toast.error({ title: t('transport.carriers.toast.error'), message })
            }
            else {
                mainErrors.handleApiError(error, { fallbackMessage: t('transport.carriers.toast.error') })
            }
            return false
        }
        finally {
            mainSubmitting.value = false
        }
    }

    /**
     * Pré-remplit le formulaire depuis le détail `GET /api/carriers/{id}` (écran
     * Modification) : peuple carrierId + principal + adresses / contacts / prix via les
     * mappers, passe en `editMode` (navigation libre, tous onglets accessibles, bloc
     * principal éditable). Au moins un bloc Adresse / Contact affiché même sans donnée.
     */
    function prefillFrom(detail: CarrierDetail): void {
        carrierId.value = detail.id
        editMode.value = true
        mainLocked.value = false
        unlockedIndex.value = tabKeys.value.length - 1

        Object.assign(main, mapMainToDraft(detail))

        const mappedAddresses = (detail.addresses ?? []).map(mapAddressToDraft)
        addresses.value = mappedAddresses.length > 0 ? mappedAddresses : [emptyCarrierAddress()]

        const mappedContacts = (detail.contacts ?? []).map(mapContactToDraft)
        contacts.value = mappedContacts.length > 0 ? mappedContacts : [emptyCarrierContact()]

        prices.value = (detail.prices ?? []).map(mapPriceToDraft)
    }

    /**
     * PATCH partiel du transporteur (mode strict : un seul groupe de sérialisation
     * par appel — spec-back § 2.9). Servira les onglets à champs scalaires des
     * tickets suivants. No-op tant que le transporteur n'existe pas.
     */
    async function patchCarrier(payload: Record<string, unknown>): Promise<void> {
        if (carrierId.value === null) return
        await api.patch(`/carriers/${carrierId.value}`, payload, { toast: false })
    }

    /** Remontée d'erreur de suppression immédiate d'une sous-ressource (toast dédié). */
    function notifyRemovalError(error: unknown): void {
        toast.error({
            title: t('transport.carriers.toast.error'),
            message: extractApiErrorMessage((error as { data?: unknown })?.data) || t('transport.carriers.toast.error'),
        })
    }

    /**
     * Soumet TOUS les blocs d'une collection en collectant les erreurs PAR INDEX :
     * on n'arrête pas au premier bloc en échec (décision ERP-101). Réinitialise la
     * cible, tente chaque ligne via `saveRow`, mappe les 422 inline ou délègue le
     * fallback à `onUnmappedError`. `shouldSkip` ignore les amorces vides. Retourne
     * true si au moins un bloc a échoué. Miroir de `useProviderForm.submitRows`.
     */
    async function submitRows<T>(
        rows: T[],
        target: Ref<Record<string, string>[]>,
        saveRow: (row: T, index: number) => Promise<void>,
        onUnmappedError: (error: unknown, index: number) => void,
        shouldSkip?: (row: T, index: number) => boolean,
    ): Promise<boolean> {
        target.value = []
        let hasError = false
        for (let index = 0; index < rows.length; index++) {
            const row = rows[index] as T
            if (shouldSkip?.(row, index)) {
                continue
            }
            try {
                await saveRow(row, index)
            }
            catch (error) {
                const response = (error as { response?: { status?: number, _data?: unknown } })?.response
                const mapped = response?.status === 422 ? mapViolationsToRecord(response._data) : {}
                if (Object.keys(mapped).length > 0) {
                    target.value[index] = mapped
                }
                else {
                    onUnmappedError(error, index)
                }
                hasError = true
            }
        }
        return hasError
    }

    // ── Onglet Adresses (ERP-167) ─────────────────────────────────────────────
    const addresses = ref<CarrierAddressFormDraft[]>([emptyCarrierAddress()])
    // Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
    const addressErrors = ref<Record<string, string>[]>([])

    // « + Nouvelle adresse » désactivé tant que la dernière adresse n'est pas
    // complète (CP + ville + rue — RG-4.05, gate d'ajout).
    const canAddAddress = computed(() => {
        const last = addresses.value[addresses.value.length - 1]
        return last !== undefined && isCarrierAddressValid(last)
    })

    function addAddress(): void {
        if (canAddAddress.value) {
            addresses.value.push(emptyCarrierAddress())
        }
    }

    /** Suppression immédiate d'une adresse existante (DELETE /carrier_addresses/{id}). */
    async function removeAddress(index: number): Promise<void> {
        await removeCollectionRow({
            rows: addresses.value,
            errors: addressErrors.value,
            index,
            endpoint: '/carrier_addresses',
            deleteRow: url => api.delete(url, {}, { toast: false }),
            makeEmpty: emptyCarrierAddress,
            onError: notifyRemovalError,
        })
    }

    /**
     * Valide l'onglet Adresses : POST des nouvelles adresses sur
     * /carriers/{id}/addresses, PATCH des existantes sur /carrier_addresses/{id}
     * (groupe carrier:write:addresses). Erreurs 422 collectées par ligne (RG-4.05
     * « obligatoire si affrété » re-validée back). Retourne true si l'onglet a été
     * validé (avancé/terminé).
     */
    async function submitAddresses(onError: (error: unknown) => void): Promise<boolean> {
        if (carrierId.value === null || tabSubmitting.value) {
            return false
        }
        tabSubmitting.value = true
        try {
            const hasError = await submitRows(
                addresses.value,
                addressErrors,
                async (address) => {
                    const body = buildCarrierAddressPayload(address)
                    if (address.id === null) {
                        const created = await api.post<{ id: number }>(
                            `/carriers/${carrierId.value}/addresses`,
                            body,
                            { headers: { Accept: 'application/ld+json' }, toast: false },
                        )
                        address.id = created.id
                    }
                    else {
                        await api.patch(`/carrier_addresses/${address.id}`, body, { toast: false })
                    }
                },
                onError,
            )
            if (hasError) {
                return false
            }
            completeTab('addresses')
            return true
        }
        finally {
            tabSubmitting.value = false
        }
    }

    // ── Onglet Contacts (ERP-168) ─────────────────────────────────────────────
    const contacts = ref<CarrierContactFormDraft[]>([emptyCarrierContact()])
    // Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
    const contactErrors = ref<Record<string, string>[]>([])

    // « + Nouveau contact » désactivé tant que le DERNIER bloc n'est pas « nommé »
    // (prénom OU nom) — aligné sur M1/M2/M3 (fonction / téléphone / email seuls ne
    // suffisent pas à ajouter un nouveau bloc).
    const canAddContact = computed(() => {
        const last = contacts.value[contacts.value.length - 1]
        return last !== undefined && isCarrierContactNamed(last)
    })

    function addContact(): void {
        if (canAddContact.value) {
            contacts.value.push(emptyCarrierContact())
        }
    }

    /** Suppression immédiate d'un contact existant (DELETE /carrier_contacts/{id}). */
    async function removeContact(index: number): Promise<void> {
        await removeCollectionRow({
            rows: contacts.value,
            errors: contactErrors.value,
            index,
            endpoint: '/carrier_contacts',
            deleteRow: url => api.delete(url, {}, { toast: false }),
            makeEmpty: emptyCarrierContact,
            onError: notifyRemovalError,
        })
    }

    /**
     * Valide l'onglet Contacts : POST des nouveaux contacts sur
     * /carriers/{id}/contacts, PATCH des existants sur /carrier_contacts/{id}
     * (groupe carrier:write:contacts). RG-4.08 (≥ 1 champ rempli, max 2 téléphones)
     * re-validée back → 422 par ligne. Si l'onglet ne contient QUE des amorces
     * vides, on soumet la 1re pour déclencher la 422 RG-4.08 inline plutôt que de
     * finaliser un onglet vide. Retourne true si l'onglet a été validé.
     */
    async function submitContacts(onError: (error: unknown) => void): Promise<boolean> {
        if (carrierId.value === null || tabSubmitting.value) {
            return false
        }
        tabSubmitting.value = true
        try {
            const hasSubmittable = contacts.value.some(c => c.id !== null || !isCarrierContactBlank(c))
            const hasError = await submitRows(
                contacts.value,
                contactErrors,
                async (contact) => {
                    const body = buildCarrierContactPayload(contact)
                    if (contact.id === null) {
                        const created = await api.post<{ id: number }>(
                            `/carriers/${carrierId.value}/contacts`,
                            body,
                            { headers: { Accept: 'application/ld+json' }, toast: false },
                        )
                        contact.id = created.id
                    }
                    else {
                        await api.patch(`/carrier_contacts/${contact.id}`, body, { toast: false })
                    }
                },
                onError,
                // Amorce vide neuve ignorée s'il reste un autre bloc soumettable ;
                // sinon on la soumet pour déclencher la 422 RG-4.08 (sur firstName).
                contact => hasSubmittable && contact.id === null && isCarrierContactBlank(contact),
            )
            if (hasError) {
                return false
            }
            completeTab('contacts')
            return true
        }
        finally {
            tabSubmitting.value = false
        }
    }

    // ── Onglet Prix (ERP-169) ─────────────────────────────────────────────────
    // Un bloc présent par défaut (sens CLIENT pré-sélectionné). L'utilisateur ajoute
    // les suivants via « + Nouveau prix ».
    const prices = ref<CarrierPriceFormDraft[]>([emptyCarrierPrice()])
    // Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
    const priceErrors = ref<Record<string, string>[]>([])

    // « + Nouveau prix » : autorisé si la liste est vide, sinon le dernier bloc doit
    // être valide (branche complète + prix — RG-4.09→4.11, pré-check léger).
    const canAddPrice = computed(() => {
        const last = prices.value[prices.value.length - 1]
        return last === undefined || isCarrierPriceValid(last)
    })

    function addPrice(): void {
        if (canAddPrice.value) {
            prices.value.push(emptyCarrierPrice())
        }
    }

    /**
     * Pré-validation FRONT d'un bloc prix (ERP-101) : renvoie les erreurs inline par
     * champ obligatoire (sens + branche active + communs). Nécessaire car côté back
     * l'Assert\NotBlank sur les scalaires (price/priceState...) court-circuite la
     * validation de branche du CarrierPriceProcessor : le 422 ne porterait jamais
     * client/supplier/adresses en même temps. Messages alignés sur le back.
     */
    function validatePriceRow(price: CarrierPriceFormDraft): Record<string, string> {
        const errs: Record<string, string> = {}
        const msg = (key: string): string => t(`transport.carriers.form.price.errors.${key}`)

        if (!price.direction) {
            errs.direction = msg('direction')
        }
        if (price.direction === 'CLIENT') {
            if (!price.clientIri) errs.client = msg('client')
            if (!price.clientDeliveryAddressIri) errs.clientDeliveryAddress = msg('clientDeliveryAddress')
            if (!price.departureSiteIri) errs.departureSite = msg('departureSite')
        }
        if (price.direction === 'FOURNISSEUR') {
            if (!price.supplierIri) errs.supplier = msg('supplier')
            if (!price.supplierSupplyAddressIri) errs.supplierSupplyAddress = msg('supplierSupplyAddress')
            if (!price.deliverySiteIri) errs.deliverySite = msg('deliverySite')
        }
        if (!price.containerType) errs.containerType = msg('containerType')
        if (!price.pricingUnit) errs.pricingUnit = msg('pricingUnit')
        if (!price.price || price.price.trim() === '') errs.price = msg('price')
        if (!price.priceState) errs.priceState = msg('priceState')

        return errs
    }

    /** Suppression immédiate d'un prix existant (DELETE /carrier_prices/{id}). */
    async function removePrice(index: number): Promise<void> {
        await removeCollectionRow({
            rows: prices.value,
            errors: priceErrors.value,
            index,
            endpoint: '/carrier_prices',
            deleteRow: url => api.delete(url, {}, { toast: false }),
            makeEmpty: emptyCarrierPrice,
            onError: notifyRemovalError,
        })
    }

    /**
     * Valide l'onglet Prix : POST des nouveaux prix sur /carriers/{id}/prices, PATCH
     * des existants sur /carrier_prices/{id} (groupe carrier:write:prices). La
     * cohérence de branche CLIENT/FOURNISSEUR (RG-4.09→4.11) est re-validée back →
     * 422 par ligne. Onglet Prix optionnel : une liste vide finalise sans appel.
     * Retourne true si l'onglet a été validé (création terminée).
     */
    async function submitPrices(onError: (error: unknown) => void): Promise<boolean> {
        if (carrierId.value === null || tabSubmitting.value) {
            return false
        }

        // Pré-check front : affiche toutes les obligations sous leur champ d'un coup
        // (le back ne peut pas tout renvoyer en une passe — cf. validatePriceRow).
        const frontErrors = prices.value.map(validatePriceRow)
        if (frontErrors.some(errs => Object.keys(errs).length > 0)) {
            priceErrors.value = frontErrors
            return false
        }

        tabSubmitting.value = true
        try {
            const hasError = await submitRows(
                prices.value,
                priceErrors,
                async (price) => {
                    const body = buildCarrierPricePayload(price)
                    if (price.id === null) {
                        const created = await api.post<{ id: number }>(
                            `/carriers/${carrierId.value}/prices`,
                            body,
                            { headers: { Accept: 'application/ld+json' }, toast: false },
                        )
                        price.id = created.id
                    }
                    else {
                        await api.patch(`/carrier_prices/${price.id}`, body, { toast: false })
                    }
                },
                onError,
            )
            if (hasError) {
                return false
            }
            completeTab('prices')
            return true
        }
        finally {
            tabSubmitting.value = false
        }
    }

    /**
     * Intègre une ligne QUALIMAT sélectionnée dans l'onglet Qualimat (RG-4.01 /
     * § 2.5) : copie le nom, force la certification à « QUALIMAT » (lecture seule),
     * pose la FK `qualimatCarrier` (IRI) et copie l'adresse (pour l'onglet Adresses).
     * Si le transporteur existe déjà (post-POST, cas nominal de l'onglet), persiste
     * d'abord la copie via un PATCH partiel `carrier:write:main` : la copie locale
     * (nom, certification figée « QUALIMAT », FK, adresse) n'est appliquée qu'en cas
     * de succès, pour ne pas laisser l'UI dans un état QUALIMAT non sauvegardé si le
     * PATCH échoue. Retourne true si l'intégration a abouti.
     */
    async function applyQualimatSelection(row: QualimatCarrierRow): Promise<boolean> {
        // Transporteur déjà créé : on persiste avant de refléter localement.
        if (carrierId.value !== null) {
            try {
                await patchCarrier({
                    qualimatCarrier: row['@id'],
                    name: row.name,
                    certificationType: 'QUALIMAT',
                })
            }
            catch (error) {
                mainErrors.handleApiError(error, { fallbackMessage: t('transport.carriers.toast.error') })
                return false
            }
        }

        main.name = row.name ?? ''
        main.certificationType = 'QUALIMAT'
        main.qualimatCarrierIri = row['@id']
        qualimatAddress.value = {
            country: 'France',
            postalCode: row.postalCode ?? '',
            city: row.city ?? '',
            street: row.address ?? '',
        }
        // RG-4.05 : pré-remplit le 1er bloc de l'onglet Adresses par copie (la FK
        // QUALIMAT survit, les champs restent éditables — § 2.5).
        addresses.value = [{
            id: null,
            country: 'France',
            postalCode: row.postalCode || null,
            city: row.city || null,
            street: row.address || null,
            streetComplement: null,
        }]
        return true
    }

    /**
     * Marque un onglet validé (passe en lecture seule), déverrouille et avance à
     * l'onglet suivant. Retourne true si c'était le dernier onglet du flux (création
     * terminée), false sinon.
     */
    function completeTab(key: string): boolean {
        // En modification : navigation libre, l'onglet reste éditable après validation.
        if (editMode.value) {
            return false
        }
        validated[key] = true
        const index = tabIndex(key)
        const next = tabKeys.value[index + 1]
        if (next === undefined) {
            return true
        }
        unlockedIndex.value = Math.max(unlockedIndex.value, index + 1)
        activeTab.value = next
        return false
    }

    return {
        // état
        main,
        qualimatAddress,
        carrierId,
        mainLocked,
        mainSubmitting,
        tabSubmitting,
        mainErrors,
        // affichage conditionnel
        isLiot,
        isQualimat,
        showCertification,
        certificationReadonly,
        showCharteredFields,
        showDischarge,
        // onglets
        tabKeys,
        activeTab,
        unlockedIndex,
        validated,
        editMode,
        isValidated,
        // adresses
        addresses,
        addressErrors,
        canAddAddress,
        addAddress,
        removeAddress,
        submitAddresses,
        // contacts
        contacts,
        contactErrors,
        canAddContact,
        addContact,
        removeContact,
        submitContacts,
        // prix
        prices,
        priceErrors,
        canAddPrice,
        addPrice,
        removePrice,
        submitPrices,
        // actions
        validateMainFront,
        buildMainPayload,
        submitMain,
        updateMain,
        prefillFrom,
        patchCarrier,
        applyQualimatSelection,
        completeTab,
        submitRows,
    }
}
