<template>
    <div>
        <div class="flex items-center justify-between">
            <h1 class="text-xl font-bold text-primary-500 sm:text-2xl">
                {{ t('admin.auditLog.title') }}
            </h1>
        </div>

        <!-- Filtres -->
        <section class="mt-4 rounded border border-gray-200 bg-white p-4">
            <!-- Labels uniformes au-dessus : les composants Malio sont utilises sans
                 leur `label` flottant interne pour ne pas mixer deux patterns de label. -->
            <div class="grid grid-cols-1 items-start gap-3 md:grid-cols-5">
                <!-- TODO(malio-ui): remplacer par un composant Malio quand la lib
                     exposera un datetime picker. Cf. exception documentee dans
                     CLAUDE.md (section "Composants formulaires"). -->
                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-600">
                        {{ t('audit.filters.date_from') }}
                    </label>
                    <input
                        v-model="filters.performedAtAfter"
                        type="datetime-local"
                        class="h-[40px] w-full rounded-md border border-m-muted bg-white px-3 text-sm outline-none focus-visible:border-2 focus-visible:border-m-primary"
                    >
                </div>
                <!-- TODO(malio-ui): idem ci-dessus. -->
                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-600">
                        {{ t('audit.filters.date_to') }}
                    </label>
                    <input
                        v-model="filters.performedAtBefore"
                        type="datetime-local"
                        class="h-[40px] w-full rounded-md border border-m-muted bg-white px-3 text-sm outline-none focus-visible:border-2 focus-visible:border-m-primary"
                    >
                </div>
                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-600">
                        {{ t('audit.filters.entity_type') }}
                    </label>
                    <div class="[&>div>div]:!mt-0">
                        <MalioSelectCheckbox
                            v-model="selectedEntityTypes"
                            :options="entityTypeOptions"
                            :display-select-all="true"
                            :display-tag="true"
                            min-width="w-full"
                            text-field="text-sm"
                            text-value="text-sm"
                        />
                    </div>
                </div>
                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-600">
                        {{ t('audit.filters.user') }}
                    </label>
                    <MalioInputText
                        v-model="performedByInput"
                        icon-name="mdi:account-search"
                        input-class="text-sm"
                        group-class="h-10"
                    />
                </div>
                <!-- TODO(malio-ui): remplacer par MalioSelect quand la lib
                     supportera de maniere fiable des options a valeur string
                     (cf. note Lesstime CLAUDE.md). Exception documentee dans
                     CLAUDE.md (section "Composants formulaires"). -->
                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-600">
                        {{ t('audit.filters.action') }}
                    </label>
                    <select
                        v-model="actionValue"
                        class="h-[40px] w-full rounded-md border border-m-muted bg-white px-3 text-sm outline-none focus-visible:border-2 focus-visible:border-m-primary"
                    >
                        <option value="">{{ t('audit.filters.all_actions') }}</option>
                        <option v-for="opt in actionOptions" :key="opt.value" :value="opt.value">
                            {{ opt.label }}
                        </option>
                    </select>
                </div>
            </div>

            <div class="mt-3 flex justify-end">
                <MalioButton
                    variant="tertiary"
                    :label="t('audit.filters.reset')"
                    button-class="text-xs"
                    @click="resetFilters"
                />
            </div>
        </section>

        <!-- Tableau -->
        <MalioDataTable
            class="mt-4"
            :columns="columns"
            :items="rows"
            :total-items="totalItems"
            :page="filters.page ?? 1"
            :per-page="filters.itemsPerPage ?? 10"
            :per-page-options="[10, 25, 50]"
            :empty-message="isFiltered ? t('audit.no_results') : t('audit.empty')"
            @update:page="onPageChange"
            @update:per-page="onPerPageChange"
            @row-click="onRowClick"
        >
            <template #cell-action="{ item }">
                <span
                    class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
                    :class="actionBadgeClass(item.action as string)"
                >
                    {{ t(`audit.action.${item.action}`) }}
                </span>
            </template>
            <template #cell-entityType="{ item }">
                <span class="font-mono text-xs">{{ item.entityType }}</span>
            </template>
            <template #cell-entityId="{ item }">
                <span class="font-mono text-xs">{{ item.entityId }}</span>
            </template>
            <template #cell-summary="{ item }">
                <span class="text-xs text-gray-600">{{ item.summary }}</span>
            </template>
        </MalioDataTable>

        <!-- Drawer detail : diff courant + timeline complete de l'entite -->
        <MalioDrawer
            v-model="drawerOpen"
            :title="drawerTitle"
            drawer-class="max-w-2xl"
        >
            <div v-if="selectedEntry">
                <AuditLogDetail :entry="selectedEntry" />
                <div class="mt-4 border-t border-gray-200 pt-3">
                    <h3 class="text-sm font-medium text-gray-700 mb-2">
                        {{ selectedEntry.entityType }} #{{ selectedEntry.entityId }}
                    </h3>
                    <AuditTimeline
                        :entity-type="selectedEntry.entityType"
                        :entity-id="selectedEntry.entityId"
                    />
                </div>
            </div>
        </MalioDrawer>
    </div>
</template>

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

const { t } = useI18n()
const { can } = usePermissions()
const { fetchLogs, fetchEntityTypes } = useAuditLog()

// Protection cote UI : le middleware `modules.global.ts` filtre deja les
// routes desactivees, mais si quelqu'un atterit ici sans la permission on
// renvoie une 403 plutot que de flasher un ecran vide.
if (!can('core.audit_log.view')) {
    throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
}

useHead({ title: t('admin.auditLog.title') })

// Etat des filtres : local uniquement, JAMAIS persiste dans l'URL (cf. regle
// CLAUDE.md "Tableau : pas de persistance URL").
const filters = reactive<AuditLogFilters>({
    performedAtAfter:  undefined,
    performedAtBefore: undefined,
    entityType:        undefined,
    performedBy:       undefined,
    action:            undefined,
    page:              1,
    itemsPerPage:      10,
})

// Multi-selection entity_type : bind dedie au MalioSelectCheckbox.
// Attention : les composants Malio attendent `{ label, value }` (pas `{ text }`).
const selectedEntityTypes = ref<(string | number)[]>([])
const entityTypes = ref<string[]>([])
const entityTypeOptions = computed(() =>
    entityTypes.value.map(t => ({ value: t, label: t })),
)

// Bind champ performedBy : MalioInputText attend `string | null`, on ne peut
// pas binder directement un `string | undefined` reactive.
const performedByInput = ref<string>('')

// Action : MalioSelect ne gere pas fiablement des options a valeur string (cf.
// note Lesstime CLAUDE.md). On utilise un `<select>` natif stylise comme les
// inputs dates pour garder un look coherent. '' = "toutes les actions".
const actionValue = ref<string>('')
const actionOptions = [
    { value: 'create', label: t('audit.action.create') },
    { value: 'update', label: t('audit.action.update') },
    { value: 'delete', label: t('audit.action.delete') },
]

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

const drawerOpen = ref(false)
const selectedEntry = ref<AuditLogEntry | null>(null)

const columns = [
    { key: 'performedAt', label: t('admin.auditLog.table.performedAt') },
    { key: 'performedBy', label: t('admin.auditLog.table.performedBy') },
    { key: 'entityType',  label: t('admin.auditLog.table.entityType') },
    { key: 'entityId',    label: t('admin.auditLog.table.entityId') },
    { key: 'action',      label: t('admin.auditLog.table.action') },
    { key: 'summary',     label: t('admin.auditLog.table.summary') },
]

// Transforme chaque AuditLogEntry en ligne compatible MalioDataTable.
// On conserve `id` pour retrouver l'entry complete sur row-click.
const rows = computed(() =>
    entries.value.map(entry => ({
        id: entry.id,
        performedAt: formatDate(entry.performedAt),
        performedBy: entry.performedBy,
        entityType:  entry.entityType,
        entityId:    entry.entityId,
        action:      entry.action,
        summary:     summarize(entry),
    })),
)

const drawerTitle = computed(() =>
    selectedEntry.value
        ? `${selectedEntry.value.entityType} #${selectedEntry.value.entityId}`
        : t('audit.detail_title'),
)

const isFiltered = computed(() =>
    Boolean(filters.performedAtAfter || filters.performedAtBefore
        || (Array.isArray(filters.entityType) ? filters.entityType.length : filters.entityType)
        || filters.performedBy || filters.action),
)

// Anti-race : chaque fetch incremente un compteur ; seul le dernier en date
// ecrit les resultats dans `entries`/`totalItems`. Evite qu'une reponse tardive
// (reseau lent) n'ecrase les resultats d'une requete ulterieure.
let requestToken = 0

// Pendant un reset, on suspend temporairement les watchers pour ne pas
// declencher 4 fetchs paralleles (un par champ mute). Les watchers Vue 3
// sont asynchrones (microtask) : il faut attendre un `nextTick` avant de
// les relacher, sinon le flag est deja `false` au moment ou ils s'executent
// et les fetchs partent quand meme. Un seul loadEntries() est appele
// explicitement apres la liberation.
let watchersSuspended = false

async function resetFilters(): Promise<void> {
    watchersSuspended = true
    filters.performedAtAfter = undefined
    filters.performedAtBefore = undefined
    filters.entityType = undefined
    filters.performedBy = undefined
    filters.action = undefined
    filters.page = 1
    selectedEntityTypes.value = []
    performedByInput.value = ''
    actionValue.value = ''
    // Les watchers mute de Vue 3 se planifient en microtask : on attend
    // leur execution avec le flag `true`, puis on libere.
    await nextTick()
    watchersSuspended = false
    loadEntries()
}

async function loadEntries(): Promise<void> {
    const token = ++requestToken
    loading.value = true
    try {
        const data = await fetchLogs({
            ...filters,
            // Convertit datetime-local (YYYY-MM-DDTHH:MM) en ISO pour l'API.
            performedAtAfter:  filters.performedAtAfter ? toIso(filters.performedAtAfter) : undefined,
            performedAtBefore: filters.performedAtBefore ? toIso(filters.performedAtBefore) : undefined,
        })
        // Reponse obsolete (un fetch plus recent a ete lance entre-temps) :
        // on ignore le resultat pour ne pas overwrite l'etat courant.
        if (token !== requestToken) return
        entries.value = data.member ?? []
        totalItems.value = data.totalItems ?? 0
    } finally {
        if (token === requestToken) {
            loading.value = false
        }
    }
}

// Debounce utilitaire pour le champ texte performedBy : evite un refetch a
// chaque frappe (reseau + SQL) et laisse l'utilisateur finir sa saisie.
function debounce<T extends (...args: never[]) => void>(fn: T, delay: number): T {
    let timer: ReturnType<typeof setTimeout> | null = null
    return ((...args: Parameters<T>) => {
        if (null !== timer) clearTimeout(timer)
        timer = setTimeout(() => fn(...args), delay)
    }) as T
}

const debouncedReload = debounce(() => loadEntries(), 300)

function toIso(localDateTime: string): string {
    // datetime-local n'a pas de timezone : on assume heure locale et on
    // laisse le navigateur generer l'ISO via Date().
    return new Date(localDateTime).toISOString()
}

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

function actionBadgeClass(action: string): string {
    switch (action) {
        case 'create': return 'bg-green-100 text-green-800'
        case 'update': return 'bg-yellow-100 text-yellow-800'
        case 'delete': return 'bg-red-100 text-red-800'
        default: return 'bg-gray-100 text-gray-800'
    }
}

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

function onRowClick(item: Record<string, unknown>): void {
    const entry = entries.value.find(e => e.id === item.id)
    if (entry) {
        selectedEntry.value = entry
        drawerOpen.value = true
    }
}

function onPageChange(value: number): void {
    filters.page = value
    loadEntries()
}

function onPerPageChange(value: number): void {
    filters.itemsPerPage = value
    filters.page = 1
    loadEntries()
}

// Sync MalioSelectCheckbox -> filters.entityType + reset page 1 + reload.
watch(selectedEntityTypes, values => {
    if (watchersSuspended) return
    filters.entityType = values.length > 0 ? values.map(v => String(v)) : undefined
    filters.page = 1
    loadEntries()
})

// Sync select action natif -> filters.action.
watch(actionValue, value => {
    if (watchersSuspended) return
    filters.action = value === '' ? undefined : value
    filters.page = 1
    loadEntries()
})

// Sync performedBy : frappe utilisateur -> debounce 300ms pour eviter un
// refetch par caractere. Le reset passe par debouncedReload egalement pour
// coalescer si plusieurs watchers tirent en meme temps.
watch(performedByInput, value => {
    if (watchersSuspended) return
    filters.performedBy = value === '' ? undefined : value
    filters.page = 1
    debouncedReload()
})

// Synchronisation reactive : tout changement de dates declenche un fetch +
// reset de la pagination a la page 1.
watch(
    () => [filters.performedAtAfter, filters.performedAtBefore],
    () => {
        if (watchersSuspended) return
        filters.page = 1
        loadEntries()
    },
)

onMounted(async () => {
    // Charge les entity types en parallele de la liste principale : un
    // echec du premier endpoint (ex: reseau flaky) ne doit pas empecher
    // le tableau d'audit de s'afficher. En cas d'erreur, on laisse le
    // filtre vide — l'utilisateur pourra quand meme consulter le journal.
    try {
        entityTypes.value = await fetchEntityTypes()
    } catch {
        entityTypes.value = []
    }
    await loadEntries()
})
</script>
