<template>
    <!--
        Garde permission : aucun rendu DOM ni appel API si l'utilisateur n'a
        pas le droit. On wrappe le contenu dans un bloc v-if plutot qu'un div
        vide pour eviter de polluer la layout quand le composant est embarque
        dans une page qui rend deja sa propre structure.
    -->
    <div v-if="canView" class="audit-timeline">
        <!-- Skeleton loader initial -->
        <ul v-if="loading && entries.length === 0" class="space-y-3">
            <li v-for="i in 3" :key="i" class="flex gap-3">
                <div class="h-3 w-3 rounded-full bg-gray-200 animate-pulse mt-1.5" />
                <div class="flex-1 space-y-2">
                    <div class="h-3 w-1/3 rounded bg-gray-200 animate-pulse" />
                    <div class="h-2 w-2/3 rounded bg-gray-100 animate-pulse" />
                </div>
            </li>
        </ul>

        <p
            v-else-if="!loading && entries.length === 0"
            class="text-sm text-gray-500 italic"
        >
            {{ t('audit.timeline.empty') }}
        </p>

        <ul v-else class="relative border-l-2 border-gray-200 pl-6 space-y-5">
            <li
                v-for="entry in entries"
                :key="entry.id"
                class="relative"
            >
                <!-- Dot sur la barre verticale. Couleur selon action. -->
                <span
                    class="absolute -left-[31px] top-1 h-3 w-3 rounded-full ring-2 ring-white"
                    :class="dotClass(entry.action)"
                />

                <div class="flex items-start justify-between gap-4">
                    <div class="flex-1 min-w-0">
                        <p class="text-sm">
                            <span class="font-medium">{{ entry.performedBy }}</span>
                            <span class="text-gray-500"> — {{ t(`audit.action.${entry.action}`) }}</span>
                        </p>

                        <!-- Update : diff field-by-field. Create/Delete : liste des champs. -->
                        <div v-if="entry.action === 'update'" class="mt-1 text-xs text-gray-600 space-y-0.5">
                            <div v-for="(diff, field) in updateDiff(entry)" :key="field">
                                <span class="font-medium">{{ field }}</span> :
                                <span class="line-through text-red-600">{{ formatValue(diff.old) }}</span>
                                <span class="mx-1">→</span>
                                <span class="text-green-700">{{ formatValue(diff.new) }}</span>
                            </div>
                            <!-- Modifications de collections to-many. -->
                            <div v-for="(diff, field) in collectionDiff(entry)" :key="`col-${field}`">
                                <span class="font-medium">{{ field }}</span> :
                                <span v-if="diff.removed.length" class="text-red-600">−{{ diff.removed.join(', ') }}</span>
                                <span v-if="diff.removed.length && diff.added.length" class="mx-1"> </span>
                                <span v-if="diff.added.length" class="text-green-700">+{{ diff.added.join(', ') }}</span>
                            </div>
                        </div>
                        <div v-else class="mt-1 text-xs text-gray-600">
                            {{ snapshotSummary(entry) }}
                        </div>
                    </div>

                    <!-- Date relative FR + tooltip absolu -->
                    <time
                        :title="absoluteDate(entry.performedAt)"
                        class="shrink-0 text-xs text-gray-500"
                    >
                        {{ relativeDate(entry.performedAt) }}
                    </time>
                </div>
            </li>
        </ul>

        <!-- Lazy loading : bouton "Voir plus" si plus de pages. -->
        <div v-if="hasMore" class="mt-4 flex justify-center">
            <button
                type="button"
                class="px-3 py-1.5 text-sm rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-60"
                :disabled="loading"
                @click="loadMore"
            >
                {{ loading ? t('common.loading') : t('audit.timeline.load_more') }}
            </button>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue'
import type { AuditLogEntry } from '~/shared/types'

const props = defineProps<{
    entityType: string
    entityId: string | number
}>()

const { entityType, entityId } = toRefs(props)

const { t } = useI18n()
const { can } = usePermissions()
const { fetchEntityLogs } = useAuditLog()

const canView = computed(() => can('core.audit_log.view'))

const entries = ref<AuditLogEntry[]>([])
const page = ref(1)
const totalItems = ref(0)
const loading = ref(false)

// Lazy loading : 10 items par page cote UX. On aligne la pagination backend
// (itemsPerPage=10 dans fetchEntityLogs) avec cette taille pour eviter de
// slicer cote client — sinon les items 11-30 de chaque page etaient ignores.
const PAGE_SIZE = 10

// Anti-race : un utilisateur qui change rapidement d'entite affichee (ouvre
// une ligne puis une autre dans le tableau admin) peut declencher deux fetchs
// dont le premier repond en retard et ecrase l'etat de la seconde timeline.
// On incremente un token a chaque fetch ; seule la derniere requete ecrit le
// resultat. loadMore() est aussi protege : une reponse tardive append sur
// une timeline dont l'entite a deja change serait visuellement confuse.
let requestToken = 0

const hasMore = computed(() => entries.value.length < totalItems.value)

async function loadPage(targetPage: number, append: boolean): Promise<void> {
    if (!canView.value) return

    const token = ++requestToken
    loading.value = true
    try {
        const data = await fetchEntityLogs(entityType.value, entityId.value, targetPage, PAGE_SIZE)
        if (token !== requestToken) return
        const items = data.member ?? []
        entries.value = append ? [...entries.value, ...items] : items
        totalItems.value = data.totalItems ?? entries.value.length
        page.value = targetPage
    } catch {
        if (token !== requestToken) return
        // Erreur silencieuse (timeline secondaire) — useApi n'affiche pas de toast avec toast: false.
        entries.value = append ? entries.value : []
    } finally {
        if (token === requestToken) {
            loading.value = false
        }
    }
}

async function loadMore(): Promise<void> {
    await loadPage(page.value + 1, true)
}

function dotClass(action: string): string {
    switch (action) {
        case 'create': return 'bg-green-500'
        case 'update': return 'bg-yellow-500'
        case 'delete': return 'bg-red-500'
        default: return 'bg-gray-400'
    }
}

// Relativise une date en francais via Intl.RelativeTimeFormat. On selectionne
// l'unite la plus grossiere possible (minutes < heures < jours < semaines).
const rtf = new Intl.RelativeTimeFormat('fr', { numeric: 'auto' })

function relativeDate(iso: string): string {
    const diffMs = Date.now() - new Date(iso).getTime()
    const diffSec = Math.round(diffMs / 1000)
    const absSec = Math.abs(diffSec)

    if (absSec < 60) return rtf.format(-Math.sign(diffSec) * Math.abs(diffSec), 'second')
    if (absSec < 3600) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 60), 'minute')
    if (absSec < 86400) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 3600), 'hour')
    if (absSec < 604800) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 86400), 'day')
    return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 604800), 'week')
}

function absoluteDate(iso: string): string {
    return new Date(iso).toLocaleString('fr-FR', {
        dateStyle: 'medium',
        timeStyle: 'short',
    })
}

function updateDiff(entry: AuditLogEntry): Record<string, { old: unknown; new: unknown }> {
    // Format attendu: { champ: { old, new } }. On filtre defensivement les
    // valeurs qui ne correspondent pas a ce shape (pas d'erreur runtime).
    const out: Record<string, { old: unknown; new: unknown }> = {}
    for (const [key, value] of Object.entries(entry.changes)) {
        if (value && typeof value === 'object' && 'old' in value && 'new' in value) {
            const diff = value as { old: unknown; new: unknown }
            out[key] = diff
        }
    }
    return out
}

function collectionDiff(entry: AuditLogEntry): Record<string, { added: unknown[]; removed: unknown[] }> {
    // Format to-many : { champ: { added: [ids], removed: [ids] } } produit
    // par AuditListener::captureCollectionChange.
    const out: Record<string, { added: unknown[]; removed: unknown[] }> = {}
    for (const [key, value] of Object.entries(entry.changes)) {
        if (value && typeof value === 'object' && 'added' in value && 'removed' in value) {
            const diff = value as { added: unknown; removed: unknown }
            out[key] = {
                added:   Array.isArray(diff.added) ? diff.added : [],
                removed: Array.isArray(diff.removed) ? diff.removed : [],
            }
        }
    }
    return out
}

function snapshotSummary(entry: AuditLogEntry): string {
    const keys = Object.keys(entry.changes)
    if (keys.length === 0) return '—'
    if (keys.length <= 4) return keys.join(', ')
    return `${keys.slice(0, 4).join(', ')}…`
}

function formatValue(value: unknown): string {
    if (value === null || value === undefined) return '∅'
    if (typeof value === 'boolean') return value ? 'oui' : 'non'
    if (typeof value === 'object') return JSON.stringify(value)
    return String(value)
}

// Reload si l'entite affichee change.
watch([entityType, entityId], () => {
    entries.value = []
    page.value = 1
    totalItems.value = 0
    loadPage(1, false)
})

onMounted(() => {
    loadPage(1, false)
})
</script>
