"""Routes du réservoir, de ses gestes et des réglages d'organisation (spec Imports §6).

`site_target_id` est **requis** sur `GET /keywords` : le réservoir se lit toujours par cible
(prédicat d'appartenance §6). Pas de vue « tout l'org » en v1 — le besoin n'est pas établi et
rendre le paramètre optionnel plus tard suffirait à l'ajouter.

Quatre routeurs cohabitent ici parce que les gestes de l'écran Réservoir portent sur quatre
ressources (§1.8) : le mot-clé lui-même (`/keywords`), ses listes (`/keyword-lists`), les tags
de l'organisation (`/tags`) et leur affectation (`/keyword-tags`).

**Écarts assumés au tableau §6**, tous demandés par l'écran T5 et sans effet sur le modèle :
`GET /tags` (le filtre par tag de la table a besoin de la liste des tags),
`DELETE /keyword-lists/{id}/items` et `DELETE /keyword-tags` (les gestes de masse sont
réversibles : retirer d'une liste, retirer un tag). Les deux `DELETE` prennent leur corps en
JSON — même forme que le `POST` symétrique, plutôt qu'une longue liste d'identifiants en
query qui buterait sur la taille d'URL d'un geste de masse.
"""

import uuid
from datetime import datetime
from typing import Annotated

from fastapi import APIRouter, Query

from app.core.auth import CurrentOrgDep
from app.db.engine import SessionDep
from app.imports import service as imports_service
from app.keywords import service
from app.keywords.schemas import (
    ItemsAdded,
    ItemsRemoved,
    KeywordIdsIn,
    KeywordListCreate,
    KeywordListOut,
    KeywordListPage,
    KeywordListsPage,
    KeywordSuggestions,
    KeywordTagsIn,
    MapIn,
    MapOut,
    MetricHistory,
    OrgSettingsOut,
    OrgSettingsUpdate,
    TagCreate,
    TagOut,
    TagsPage,
)

router = APIRouter(prefix="/keywords", tags=["keywords"])
lists_router = APIRouter(prefix="/keyword-lists", tags=["keywords"])
tags_router = APIRouter(prefix="/tags", tags=["keywords"])
keyword_tags_router = APIRouter(prefix="/keyword-tags", tags=["keywords"])
settings_router = APIRouter(prefix="/settings", tags=["settings"])


@router.get("")
async def list_keywords(
    user: CurrentOrgDep,
    session: SessionDep,
    site_target_id: uuid.UUID,
    #  Même plafond que `/keywords/suggest` : une saisie de recherche est bornée des deux
    #  côtés de la frontière, et rien ne justifie qu'un `q` de 10 000 caractères aille jusqu'à
    #  `to_tsquery`. Pas de `min_length` en revanche — le réservoir accepte une recherche d'un
    #  caractère, qu'il paie une fois sur une navigation explicite, là où la complétion la
    #  paierait à chaque frappe.
    q: Annotated[str | None, Query(max_length=200)] = None,
    list_id: uuid.UUID | None = None,
    tag_id: uuid.UUID | None = None,
    source: str | None = None,
    intent: str | None = None,
    attach_status: str | None = None,
    volume_min: int | None = None,
    volume_max: int | None = None,
    updated_since: datetime | None = None,
    view: str | None = None,
    cursor: str | None = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 50,
) -> KeywordListPage:
    # La cible est validée avant la requête : une cible d'une autre organisation est un 404,
    # jamais une page vide qui laisserait croire à un réservoir vide.
    target = await imports_service.get_target(session, user.org_id, site_target_id)
    items, next_cursor, resolved, preferred, fuzzy = await service.list_keywords(
        session,
        user.org_id,
        site_target_id=target.id,
        view=view,
        filters=service.Filters(
            q=q,
            list_id=list_id,
            tag_id=tag_id,
            source=source,
            intent=intent,
            attach_status=attach_status,
            volume_min=volume_min,
            volume_max=volume_max,
            updated_since=updated_since,
        ),
        cursor=cursor,
        limit=limit,
    )
    return KeywordListPage(
        items=items,
        next_cursor=next_cursor,
        view=resolved,
        preferred_source=preferred,
        fuzzy=fuzzy,
    )


#  **Avant** `/{keyword_id}/…` par prudence de lecture, même si aucune route ne capture
#  `/keywords/{id}` en GET aujourd'hui : le jour où l'une le fera, `suggest` ne doit pas devenir
#  un identifiant.
@router.get("/suggest")
async def suggest_keywords(
    user: CurrentOrgDep,
    session: SessionDep,
    site_target_id: uuid.UUID,
    q: Annotated[str, Query(min_length=2, max_length=200)],
) -> KeywordSuggestions:
    """Complétion du champ de recherche du réservoir (§1.7).

    `min_length=2` est une décision de coût, pas d'ergonomie : à un caractère, la tsquery
    préfixe (`a:*`) balaie une fraction énorme du GIN pour un classement qui n'apprend rien.
    Le 422 est donc le bon refus — l'écran, lui, n'appelle simplement pas avant deux frappes.

    Aucun curseur : dix lignes sont une aide à la frappe, pas un listing. Qui veut la suite
    valide sa recherche et lit le réservoir.
    """
    target = await imports_service.get_target(session, user.org_id, site_target_id)
    items = await service.suggest_keywords(session, user.org_id, site_target_id=target.id, q=q)
    return KeywordSuggestions(items=items)


@router.get("/{keyword_id}/metrics")
async def keyword_metrics(
    user: CurrentOrgDep,
    session: SessionDep,
    keyword_id: uuid.UUID,
    site_target_id: uuid.UUID,
) -> MetricHistory:
    """Historique par source d'un mot-clé — ce que le popover de la table affiche (§1.7)."""
    #  La cible est validée bien qu'elle ne filtre pas l'historique : les observations sont
    #  mutualisées à l'organisation (décision n°2). Sans cette validation, le paramètre serait
    #  décoratif et une cible d'une autre organisation répondrait 200.
    await imports_service.get_target(session, user.org_id, site_target_id)
    return await service.metric_history(session, user.org_id, keyword_id)


@router.put("/{keyword_id}/map")
async def put_map(
    user: CurrentOrgDep, session: SessionDep, keyword_id: uuid.UUID, payload: MapIn
) -> MapOut:
    """Déclare la page cible du mot-clé. `PUT` : le corps décrit l'état voulu de l'attache."""
    target = await imports_service.get_target(session, user.org_id, payload.site_target_id)
    keyword = await service.get_keyword(session, user.org_id, keyword_id)
    row = await service.upsert_manual_map(
        session,
        user.org_id,
        keyword_id=keyword.id,
        site_target_id=target.id,
        url=payload.url,
    )
    return MapOut.model_validate(row)


@router.delete("/{keyword_id}/map", status_code=204)
async def delete_map(
    user: CurrentOrgDep, session: SessionDep, keyword_id: uuid.UUID, site_target_id: uuid.UUID
) -> None:
    target = await imports_service.get_target(session, user.org_id, site_target_id)
    await service.delete_manual_map(
        session, user.org_id, keyword_id=keyword_id, site_target_id=target.id
    )


@lists_router.get("")
async def list_keyword_lists(
    user: CurrentOrgDep,
    session: SessionDep,
    site_target_id: uuid.UUID,
    cursor: str | None = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 50,
) -> KeywordListsPage:
    target = await imports_service.get_target(session, user.org_id, site_target_id)
    items, next_cursor = await service.list_keyword_lists(
        session, user.org_id, site_target_id=target.id, cursor=cursor, limit=limit
    )
    return KeywordListsPage(items=items, next_cursor=next_cursor)


@lists_router.post("", status_code=201)
async def create_keyword_list(
    user: CurrentOrgDep, session: SessionDep, payload: KeywordListCreate
) -> KeywordListOut:
    target = await imports_service.get_target(session, user.org_id, payload.site_target_id)
    return await service.create_keyword_list(
        session, user.org_id, site_target_id=target.id, name=payload.name
    )


@lists_router.post("/{list_id}/items")
async def add_list_items(
    user: CurrentOrgDep, session: SessionDep, list_id: uuid.UUID, payload: KeywordIdsIn
) -> ItemsAdded:
    added = await service.add_list_items(session, user.org_id, list_id, payload.keyword_ids)
    return ItemsAdded(added=added)


@lists_router.delete("/{list_id}/items")
async def remove_list_items(
    user: CurrentOrgDep, session: SessionDep, list_id: uuid.UUID, payload: KeywordIdsIn
) -> ItemsRemoved:
    """Retire de la liste, jamais du réservoir de l'organisation (rétention, décision n°4)."""
    removed = await service.remove_list_items(session, user.org_id, list_id, payload.keyword_ids)
    return ItemsRemoved(removed=removed)


@tags_router.get("")
async def list_tags(
    user: CurrentOrgDep,
    session: SessionDep,
    cursor: str | None = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 100,
) -> TagsPage:
    items, next_cursor = await service.list_tags(session, user.org_id, cursor=cursor, limit=limit)
    return TagsPage(items=items, next_cursor=next_cursor)


@tags_router.post("", status_code=201)
async def create_tag(user: CurrentOrgDep, session: SessionDep, payload: TagCreate) -> TagOut:
    return TagOut.model_validate(await service.create_tag(session, user.org_id, payload.name))


@keyword_tags_router.post("")
async def add_keyword_tags(
    user: CurrentOrgDep, session: SessionDep, payload: KeywordTagsIn
) -> ItemsAdded:
    added = await service.add_keyword_tags(
        session, user.org_id, payload.tag_id, payload.keyword_ids
    )
    return ItemsAdded(added=added)


@keyword_tags_router.delete("")
async def remove_keyword_tags(
    user: CurrentOrgDep, session: SessionDep, payload: KeywordTagsIn
) -> ItemsRemoved:
    removed = await service.remove_keyword_tags(
        session, user.org_id, payload.tag_id, payload.keyword_ids
    )
    return ItemsRemoved(removed=removed)


@settings_router.get("/org")
async def get_org_settings(user: CurrentOrgDep, session: SessionDep) -> OrgSettingsOut:
    settings = await service.get_org_settings(session, user.org_id)
    #  Aucune ligne n'est créée à la lecture : l'organisation sans réglage explicite répond
    #  « pas de source préférée », et les observations sortent alors dans l'ordre par date,
    #  sans qu'aucune source passe devant (§3.4).
    if settings is None:
        return OrgSettingsOut(preferred_source=None, updated_at=None)
    return OrgSettingsOut.model_validate(settings)


@settings_router.patch("/org")
async def patch_org_settings(
    user: CurrentOrgDep, session: SessionDep, payload: OrgSettingsUpdate
) -> OrgSettingsOut:
    #  `exclude_unset` : un champ absent du corps est **inchangé**, un champ à `null` est
    #  effacé. Sans lui, `PATCH {}` remettrait la source préférée à zéro.
    settings = await service.update_org_settings(
        session, user.org_id, payload.model_dump(exclude_unset=True)
    )
    if settings is None:
        return OrgSettingsOut(preferred_source=None, updated_at=None)
    return OrgSettingsOut.model_validate(settings)
