"""Administration : indicateurs, comptes, paramètres, journal d'audit, rondache."""
from __future__ import annotations

import io
import re
import unicodedata
from typing import Any, Literal

from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from fastapi.responses import Response
from pydantic import BaseModel, Field
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session

from ..core import audit, security
from ..core.db import get_db
from ..documents import branding
from ..models import AuditLog, Indicator, User
from ..services import auth as auth_service
from ..services import settings as settings_service
from ..services.auth import Principal
from .deps import ApiError, client_ip, require

router = APIRouter(prefix="/api", tags=["administration"])


# --- Indicateurs ---------------------------------------------------------------------------------

def indicator_dict(i: Indicator) -> dict[str, Any]:
    return {"id": i.id, "code": i.code, "label": i.label, "position": i.position, "is_active": i.is_active,
            "detail_kind": i.detail_kind, "detail_unit": i.detail_unit, "direction": i.direction,
            "warn_max": i.warn_max, "include_in_total": i.include_in_total, "aliases": i.aliases or []}


class IndicatorIn(BaseModel):
    label: str = Field(min_length=2, max_length=160)
    detail_kind: Literal["taux", "vitesse"] | None = None
    detail_unit: str = Field(default="", max_length=32)
    direction: Literal["hausse_favorable", "baisse_favorable"] | None = None
    warn_max: int | None = Field(default=None, ge=0, le=100000)
    include_in_total: bool = True


class IndicatorPatch(BaseModel):
    label: str | None = Field(default=None, min_length=2, max_length=160)
    is_active: bool | None = None
    detail_unit: str | None = Field(default=None, max_length=32)
    direction: Literal["hausse_favorable", "baisse_favorable", "aucun"] | None = None
    warn_max: int | None = Field(default=None, ge=-1, le=100000)  # -1 = supprimer le seuil
    include_in_total: bool | None = None
    aliases: list[str] | None = Field(default=None, max_length=20)


class ReorderIn(BaseModel):
    ids: list[int] = Field(max_length=200)


def _slug(label: str) -> str:
    s = unicodedata.normalize("NFKD", label.lower())
    s = "".join(c for c in s if not unicodedata.combining(c))
    return re.sub(r"[^a-z0-9]+", "_", s).strip("_")[:40] or "indicateur"


@router.get("/indicators")
def list_indicators(db: Session = Depends(get_db), p: Principal = Depends(require("bilan.read"))) -> dict[str, Any]:
    rows = db.execute(select(Indicator).order_by(Indicator.position, Indicator.id)).scalars()
    return {"indicators": [indicator_dict(i) for i in rows]}


@router.post("/admin/indicators")
def create_indicator(body: IndicatorIn, request: Request, db: Session = Depends(get_db),
                     p: Principal = Depends(require("indicator.manage"))) -> dict[str, Any]:
    label = " ".join(body.label.split())
    base = _slug(label)
    code, n = base, 2
    while db.execute(select(Indicator.id).where(Indicator.code == code)).first():
        code, n = f"{base}_{n}", n + 1
    position = (db.execute(select(func.max(Indicator.position))).scalar() or 0) + 1
    ind = Indicator(code=code, label=label, position=position, is_active=True, category_id=1,
                    detail_kind=body.detail_kind, detail_unit=body.detail_unit.strip(), direction=body.direction,
                    warn_max=body.warn_max, include_in_total=body.include_in_total, aliases=[])
    db.add(ind)
    db.flush()
    audit.record(db, "indicator.created", user_id=p.user.id, username=p.user.username, target_type="indicator",
                 target_id=ind.id, ip=client_ip(request), details=indicator_dict(ind))
    db.commit()
    return indicator_dict(ind)


@router.patch("/admin/indicators/{indicator_id}")
def update_indicator(indicator_id: int, body: IndicatorPatch, request: Request, db: Session = Depends(get_db),
                     p: Principal = Depends(require("indicator.manage"))) -> dict[str, Any]:
    ind = db.get(Indicator, indicator_id)
    if ind is None:
        raise ApiError(404, "Indicateur introuvable.", "not_found")
    before = indicator_dict(ind)
    if body.label is not None:
        ind.label = " ".join(body.label.split())
    if body.is_active is not None:
        ind.is_active = body.is_active
    if body.detail_unit is not None:
        ind.detail_unit = body.detail_unit.strip()
    if body.direction is not None:
        ind.direction = None if body.direction == "aucun" else body.direction
    if body.warn_max is not None:
        ind.warn_max = None if body.warn_max < 0 else body.warn_max
    if body.include_in_total is not None:
        ind.include_in_total = body.include_in_total
    if body.aliases is not None:
        ind.aliases = [a.strip()[:80] for a in body.aliases if a.strip()]
    ind.updated_at = func.now()
    db.flush()
    after = indicator_dict(ind)
    changes = {k: {"avant": before[k], "apres": after[k]} for k in after if before[k] != after[k]}
    if changes:
        audit.record(db, "indicator.updated", user_id=p.user.id, username=p.user.username,
                     target_type="indicator", target_id=ind.id, ip=client_ip(request), details=changes)
    db.commit()
    return after


@router.post("/admin/indicators/reorder")
def reorder_indicators(body: ReorderIn, request: Request, db: Session = Depends(get_db),
                       p: Principal = Depends(require("indicator.manage"))) -> dict[str, Any]:
    all_ids = set(db.execute(select(Indicator.id)).scalars())
    if set(body.ids) != all_ids or len(body.ids) != len(all_ids):
        raise ApiError(422, "La liste doit contenir chaque indicateur une seule fois.", "bad_order")
    before = list(db.execute(select(Indicator.id).order_by(Indicator.position)).scalars())
    for pos, ind_id in enumerate(body.ids, start=1):
        ind = db.get(Indicator, ind_id)
        assert ind is not None
        ind.position = pos
    audit.record(db, "indicator.reordered", user_id=p.user.id, username=p.user.username, target_type="indicator",
                 ip=client_ip(request), details={"avant": before, "apres": body.ids})
    db.commit()
    return {"ok": True}


# --- Comptes -------------------------------------------------------------------------------------

USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{2,63}$")


class UserIn(BaseModel):
    username: str = Field(max_length=64)
    display_name: str = Field(min_length=2, max_length=120)
    can_export: bool = False


class UserPatch(BaseModel):
    display_name: str | None = Field(default=None, min_length=2, max_length=120)
    can_export: bool | None = None
    is_active: bool | None = None


def user_dict(db: Session, u: User) -> dict[str, Any]:
    return {"id": u.id, "username": u.username, "display_name": u.display_name, "role": u.role_code,
            "is_active": u.is_active, "can_export": u.can_export, "mfa_enabled": u.mfa_enabled,
            "must_change_password": u.must_change_password,
            "last_login_at": u.last_login_at.isoformat() if u.last_login_at else None,
            "created_at": u.created_at.isoformat() if u.created_at else None}


def _target_user(db: Session, user_id: int, p: Principal, *, readers_only: bool) -> User:
    u = db.get(User, user_id)
    if u is None:
        raise ApiError(404, "Compte introuvable.", "not_found")
    if readers_only and u.role_code != "reader":
        raise ApiError(403, "Les comptes gestionnaires se gèrent avec la commande d'administration "
                            "(voir docs/ADMINISTRATION.md).", "admin_via_cli")
    return u


@router.get("/admin/users")
def list_users(db: Session = Depends(get_db), p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    users = db.execute(select(User).order_by(User.role_code, User.display_name)).scalars()
    return {"users": [user_dict(db, u) for u in users]}


@router.post("/admin/users")
def create_user(body: UserIn, request: Request, db: Session = Depends(get_db),
                p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    username = body.username.strip().lower()
    if not USERNAME_RE.match(username):
        raise ApiError(422, "Identifiant : 3 à 64 caractères parmi a-z, 0-9, point, tiret, tiret bas.", "bad_username")
    if db.execute(select(User.id).where(User.username == username)).first():
        raise ApiError(409, "Cet identifiant existe déjà.", "duplicate")
    temp = security.generate_password()
    u = User(username=username, display_name=" ".join(body.display_name.split()), role_code="reader",
             is_active=True, can_export=body.can_export, password_hash=security.hash_password(temp),
             must_change_password=True, created_by=p.user.id)
    db.add(u)
    db.flush()
    audit.record(db, "user.created", user_id=p.user.id, username=p.user.username, target_type="user",
                 target_id=u.id, ip=client_ip(request),
                 details={"identifiant": username, "role": "reader", "export": body.can_export})
    db.commit()
    return {"user": user_dict(db, u), "temporary_password": temp}


@router.patch("/admin/users/{user_id}")
def update_user(user_id: int, body: UserPatch, request: Request, db: Session = Depends(get_db),
                p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    deactivating = body.is_active is False
    u = _target_user(db, user_id, p, readers_only=not deactivating)
    if u.id == p.user.id and deactivating:
        raise ApiError(400, "Vous ne pouvez pas désactiver votre propre compte.", "self")
    changes: dict[str, Any] = {}
    if body.display_name is not None and body.display_name != u.display_name:
        changes["nom"] = {"avant": u.display_name, "apres": body.display_name}
        u.display_name = " ".join(body.display_name.split())
    if body.can_export is not None and body.can_export != u.can_export:
        if u.role_code != "reader":
            raise ApiError(400, "L'option d'export ne concerne que les comptes en lecture seule.", "bad_role")
        changes["export"] = {"avant": u.can_export, "apres": body.can_export}
        u.can_export = body.can_export
    if body.is_active is not None and body.is_active != u.is_active:
        if body.is_active and u.role_code != "reader":
            raise ApiError(403, "Réactivation d'un gestionnaire : utilisez la commande d'administration.",
                           "admin_via_cli")
        u.is_active = body.is_active
        u.deactivated_at = None if body.is_active else auth_service.utcnow()
        changes["actif"] = {"avant": not body.is_active, "apres": body.is_active}
        if not body.is_active:
            changes["sessions_revoquees"] = auth_service.revoke_user_sessions(db, u.id, "compte désactivé")
    if changes:
        action = "user.deactivated" if deactivating else "user.updated"
        audit.record(db, action, user_id=p.user.id, username=p.user.username, target_type="user", target_id=u.id,
                     ip=client_ip(request), details=changes)
    db.commit()
    return user_dict(db, u)


@router.post("/admin/users/{user_id}/reset-password")
def reset_password(user_id: int, request: Request, db: Session = Depends(get_db),
                   p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    u = _target_user(db, user_id, p, readers_only=True)
    temp = security.generate_password()
    u.password_hash, u.must_change_password = security.hash_password(temp), True
    n = auth_service.revoke_user_sessions(db, u.id, "réinitialisation du mot de passe")
    audit.record(db, "user.password_reset", user_id=p.user.id, username=p.user.username, target_type="user",
                 target_id=u.id, ip=client_ip(request), details={"sessions_revoquees": n})
    db.commit()
    return {"temporary_password": temp}


@router.post("/admin/users/{user_id}/revoke-sessions")
def revoke_sessions(user_id: int, request: Request, db: Session = Depends(get_db),
                    p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    u = _target_user(db, user_id, p, readers_only=False)
    n = auth_service.revoke_user_sessions(db, u.id, "révocation par un gestionnaire",
                                          p.session.id if u.id == p.user.id else None)
    audit.record(db, "user.sessions_revoked", user_id=p.user.id, username=p.user.username, target_type="user",
                 target_id=u.id, ip=client_ip(request), details={"nombre": n})
    db.commit()
    return {"revoked": n}


@router.post("/admin/users/{user_id}/reset-mfa")
def reset_mfa(user_id: int, request: Request, db: Session = Depends(get_db),
              p: Principal = Depends(require("user.manage"))) -> dict[str, Any]:
    u = _target_user(db, user_id, p, readers_only=True)
    u.mfa_enabled, u.mfa_secret_enc, u.mfa_pending_secret_enc, u.mfa_last_step = False, None, None, None
    auth_service.issue_recovery_codes(db, u)
    n = auth_service.revoke_user_sessions(db, u.id, "réinitialisation MFA")
    audit.record(db, "user.mfa_reset", user_id=p.user.id, username=p.user.username, target_type="user",
                 target_id=u.id, ip=client_ip(request), details={"sessions_revoquees": n})
    db.commit()
    return {"ok": True}


# --- Paramètres ----------------------------------------------------------------------------------

@router.get("/admin/settings")
def get_settings(db: Session = Depends(get_db), p: Principal = Depends(require("settings.manage"))) -> dict[str, Any]:
    values = settings_service.get_all(db)
    return {"settings": [{"key": k, "value": values[k], "label": d[4], "type": d[1].__name__, "min": d[2],
                          "max": d[3], "choices": sorted(settings_service.ENUMS.get(k, []))}
                         for k, d in settings_service.DEFAULTS.items()],
            "branding": branding.info()}


@router.put("/admin/settings")
def put_settings(body: dict[str, Any], request: Request, db: Session = Depends(get_db),
                 p: Principal = Depends(require("settings.manage"))) -> dict[str, Any]:
    if len(body) > 40:
        raise ApiError(422, "Trop de paramètres.", "too_many")
    try:
        diff = settings_service.set_many(db, body, p.user.id)
    except settings_service.SettingsError as e:
        raise ApiError(422, str(e), "bad_setting") from None
    if diff:
        audit.record(db, "settings.updated", user_id=p.user.id, username=p.user.username, target_type="settings",
                     ip=client_ip(request), details={k: {"avant": a, "apres": b} for k, (a, b) in diff.items()})
    db.commit()
    return get_settings(db, p)


@router.post("/admin/branding")
async def upload_branding(request: Request, file: UploadFile = File(...), db: Session = Depends(get_db),
                          p: Principal = Depends(require("settings.manage"))) -> dict[str, Any]:
    data = await file.read(2 * 1024 * 1024 + 1)
    try:
        info = branding.install(data)
    except branding.BrandingError as e:
        raise ApiError(422, str(e), "bad_logo") from None
    audit.record(db, "branding.updated", user_id=p.user.id, username=p.user.username, target_type="branding",
                 ip=client_ip(request), details=info)
    db.commit()
    return branding.info()


@router.delete("/admin/branding")
def reset_branding(request: Request, db: Session = Depends(get_db),
                   p: Principal = Depends(require("settings.manage"))) -> dict[str, Any]:
    branding.remove()
    audit.record(db, "branding.removed", user_id=p.user.id, username=p.user.username, target_type="branding",
                 ip=client_ip(request))
    db.commit()
    return branding.info()


@router.get("/branding/rondache")
def get_rondache(p: Principal = Depends(require("bilan.read"))) -> Response:
    data, mime = branding.current()
    return Response(data, media_type=mime, headers={"Cache-Control": "private, max-age=300",
                                                    "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'"})


# --- Journal d'audit -----------------------------------------------------------------------------

@router.get("/admin/audit")
def get_audit(q: str | None = Query(default=None, max_length=80), action: str | None = Query(default=None, max_length=64),
              before_id: int | None = None, limit: int = Query(default=100, ge=1, le=500),
              db: Session = Depends(get_db), p: Principal = Depends(require("audit.read"))) -> dict[str, Any]:
    stmt = select(AuditLog).order_by(AuditLog.id.desc()).limit(limit)
    if before_id:
        stmt = stmt.where(AuditLog.id < before_id)
    if action:
        stmt = stmt.where(AuditLog.action.like(f"{action.replace('%', '')}%"))
    if q:
        like = f"%{q.replace('%', '')}%"
        stmt = stmt.where(or_(AuditLog.username.ilike(like), AuditLog.target_id.ilike(like),
                              AuditLog.action.ilike(like)))
    rows = list(db.execute(stmt).scalars())
    ok, count, bad = audit.verify_chain(db)
    return {"entries": [{"id": r.id, "at": r.at.isoformat(), "username": r.username, "action": r.action,
                         "target_type": r.target_type, "target_id": r.target_id, "ip": r.ip, "details": r.details}
                        for r in rows],
            "chain": {"ok": ok, "count": count, "first_bad_id": bad}}


@router.get("/admin/audit/export.csv")
def export_audit(request: Request, db: Session = Depends(get_db),
                 p: Principal = Depends(require("audit.read"))) -> Response:
    import csv
    import json as _json

    from ..documents.exports import neutralize
    buf = io.StringIO()
    w = csv.writer(buf, delimiter=";")
    w.writerow(["id", "date", "utilisateur", "action", "cible", "identifiant", "ip", "details", "hash"])
    for r in db.execute(select(AuditLog).order_by(AuditLog.id)).scalars():
        w.writerow([r.id, r.at.isoformat(), neutralize(r.username or ""), r.action, r.target_type or "",
                    neutralize(r.target_id or ""), r.ip or "", neutralize(_json.dumps(r.details, ensure_ascii=False)),
                    r.hash])
    audit.record(db, "audit.exported", user_id=p.user.id, username=p.user.username, ip=client_ip(request))
    db.commit()
    return Response("﻿" + buf.getvalue(), media_type="text/csv; charset=utf-8",
                    headers={"Content-Disposition": 'attachment; filename="journal-audit-edcf52.csv"'})
