"""Admin API: CRUD конфигурации + просмотр потока. Всё под require_admin.

Это бэкенд SPA админки (#32+): списки и создание/удаление — то, без чего
CLI не нужен в проде. Полное редактирование правил (PATCH) — в этой же
схеме: только enabled/name/template/throttle/weight/conditions/actions.
"""

import uuid
from datetime import UTC, datetime

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import ValidationError
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session

from app.auth.apikeys import generate_token, token_hash_of
from app.auth.deps import AuthenticatedUser, require_admin
from app.config import get_settings
from app.database import get_db
from app.models import (
    ApiKey,
    ChannelTarget,
    Delivery,
    Event,
    NotificationType,
    RoutingRule,
    RoutingRuleAction,
    Source,
)
from app.api.admin_schemas import (
    AdminEventOut,
    DeliveryOut,
    KeyCreated,
    KeyIn,
    KeyOut,
    RuleActionIn,
    RuleActionOut,
    RuleIn,
    RuleOut,
    RulePatch,
    SettingOut,
    SettingsPut,
    SourceIn,
    SourceOut,
    TargetIn,
    TargetOut,
    TargetPatch,
    TypeIn,
    TypeOut,
)
from app.settings_registry import EDITABLE, is_secret, validate as validate_setting
from app.settings_store import default_of, get_setting_row, set_setting

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


def _conflict(detail: str) -> HTTPException:
    return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)


# --- sources ---

@router.get("/sources", response_model=list[SourceOut])
def list_sources(
    user: AuthenticatedUser = Depends(require_admin), db: Session = Depends(get_db)
) -> list[SourceOut]:
    sources = db.execute(select(Source).order_by(Source.name)).scalars().all()
    key_counts = dict(
        db.execute(select(ApiKey.source_id, func.count(ApiKey.id)).group_by(ApiKey.source_id)).all()
    )
    type_counts = dict(
        db.execute(
            select(NotificationType.source_id, func.count(NotificationType.id))
            .group_by(NotificationType.source_id)
        ).all()
    )
    return [
        SourceOut(
            id=s.id, name=s.name, label=s.label, description=s.description,
            created_at=s.created_at, keys_count=key_counts.get(s.id, 0),
            types_count=type_counts.get(s.id, 0),
        )
        for s in sources
    ]


@router.post("/sources", response_model=SourceOut, status_code=201)
def create_source(
    payload: SourceIn,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> SourceOut:
    if db.execute(select(Source.id).where(Source.name == payload.name)).scalar_one_or_none():
        raise _conflict(f"source '{payload.name}' уже существует")
    source = Source(**payload.model_dump())
    db.add(source)
    db.commit()
    db.refresh(source)
    return SourceOut(id=source.id, created_at=source.created_at, **payload.model_dump())


@router.delete("/sources/{source_id}", status_code=204)
def delete_source(
    source_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> None:
    source = db.get(Source, source_id)
    if source is None:
        raise HTTPException(status_code=404, detail="Источник не найден")
    events = db.execute(select(func.count(Event.id)).where(Event.source_id == source_id)).scalar_one()
    if events:
        raise _conflict(
            f"Источник имеет {events} событий — удалить нельзя (история); отзови ключи и выключи типы"
        )
    db.delete(source)
    db.commit()


# --- keys ---

@router.get("/sources/{source_id}/keys", response_model=list[KeyOut])
def list_keys(
    source_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> list[KeyOut]:
    if db.get(Source, source_id) is None:
        raise HTTPException(status_code=404, detail="Источник не найден")
    keys = db.execute(
        select(ApiKey).where(ApiKey.source_id == source_id).order_by(ApiKey.created_at)
    ).scalars().all()
    return [
        KeyOut(
            id=k.id, name=k.name, token_hint=k.token_hint, created_at=k.created_at,
            last_used_at=k.last_used_at, revoked_at=k.revoked_at,
        )
        for k in keys
    ]


@router.post("/sources/{source_id}/keys", response_model=KeyCreated, status_code=201)
def create_key(
    source_id: int,
    payload: KeyIn,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> KeyCreated:
    source = db.get(Source, source_id)
    if source is None:
        raise HTTPException(status_code=404, detail="Источник не найден")
    token = generate_token()
    key = ApiKey(
        source_id=source_id,
        name=payload.name,
        token_hash=token_hash_of(token),
        token_hint=token[-4:],
    )
    db.add(key)
    db.commit()
    db.refresh(key)
    return KeyCreated(
        id=key.id, name=key.name, token_hint=key.token_hint, created_at=key.created_at,
        last_used_at=None, revoked_at=None, token=token,
    )


@router.post("/sources/{source_id}/keys/{key_id}/revoke", response_model=KeyOut)
def revoke_key(
    source_id: int,
    key_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> KeyOut:
    key = db.get(ApiKey, key_id)
    if key is None or key.source_id != source_id:
        raise HTTPException(status_code=404, detail="Ключ не найден")
    if key.revoked_at is not None:
        raise _conflict("Ключ уже отозван")
    key.revoked_at = datetime.now(UTC)
    db.commit()
    db.refresh(key)
    return KeyOut(
        id=key.id, name=key.name, token_hint=key.token_hint, created_at=key.created_at,
        last_used_at=key.last_used_at, revoked_at=key.revoked_at,
    )


# --- types ---

@router.get("/types", response_model=list[TypeOut])
def list_types(
    user: AuthenticatedUser = Depends(require_admin), db: Session = Depends(get_db)
) -> list[TypeOut]:
    rows = (
        db.execute(
            select(NotificationType, Source.name)
            .join(Source, NotificationType.source_id == Source.id)
            .order_by(Source.name, NotificationType.subject, NotificationType.action)
        ).all()
    )
    return [
        TypeOut(
            id=nt.id, source_id=nt.source_id, source_name=source_name, subject=nt.subject,
            action=nt.action, payload_schema=nt.payload_schema, description=nt.description,
            created_at=nt.created_at,
        )
        for nt, source_name in rows
    ]


@router.post("/types", response_model=TypeOut, status_code=201)
def create_type(
    payload: TypeIn,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> TypeOut:
    source = db.get(Source, payload.source_id)
    if source is None:
        raise HTTPException(status_code=422, detail="Источник не найден")
    dup = db.execute(
        select(NotificationType.id).where(
            NotificationType.source_id == payload.source_id,
            NotificationType.subject == payload.subject,
            NotificationType.action == payload.action,
        )
    ).scalar_one_or_none()
    if dup:
        raise _conflict(f"Тип ({source.name}, {payload.subject}, {payload.action}) уже есть")
    nt = NotificationType(**payload.model_dump())
    db.add(nt)
    db.commit()
    db.refresh(nt)
    return TypeOut(id=nt.id, source_name=source.name, created_at=nt.created_at, **payload.model_dump())


@router.delete("/types/{type_id}", status_code=204)
def delete_type(
    type_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> None:
    if db.get(NotificationType, type_id) is None:
        raise HTTPException(status_code=404, detail="Тип не найден")
    db.execute(delete(NotificationType).where(NotificationType.id == type_id))
    db.commit()


# --- targets ---

@router.get("/targets", response_model=list[TargetOut])
def list_targets(
    user: AuthenticatedUser = Depends(require_admin), db: Session = Depends(get_db)
) -> list[TargetOut]:
    targets = db.execute(select(ChannelTarget).order_by(ChannelTarget.channel, ChannelTarget.name)).scalars().all()
    return [
        TargetOut(
            id=t.id, channel=t.channel, name=t.name, description=t.description,
            config=t.config, enabled=t.enabled, created_at=t.created_at,
        )
        for t in targets
    ]


@router.post("/targets", response_model=TargetOut, status_code=201)
def create_target(
    payload: TargetIn,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> TargetOut:
    dup = db.execute(
        select(ChannelTarget.id).where(
            ChannelTarget.channel == payload.channel, ChannelTarget.name == payload.name
        )
    ).scalar_one_or_none()
    if dup:
        raise _conflict(f"Цель {payload.channel}/{payload.name} уже есть")
    target = ChannelTarget(**payload.model_dump())
    db.add(target)
    db.commit()
    db.refresh(target)
    return TargetOut(id=target.id, enabled=target.enabled, created_at=target.created_at, **payload.model_dump())


@router.patch("/targets/{target_id}", response_model=TargetOut)
def patch_target(
    target_id: int,
    payload: TargetPatch,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> TargetOut:
    target = db.get(ChannelTarget, target_id)
    if target is None:
        raise HTTPException(status_code=404, detail="Цель не найдена")
    data = payload.model_dump(exclude_unset=True)
    if "name" in data and (data["name"] != target.name):
        dup = db.execute(
            select(ChannelTarget.id).where(
                ChannelTarget.channel == target.channel, ChannelTarget.name == data["name"]
            )
        ).scalar_one_or_none()
        if dup:
            raise _conflict(f"Цель {target.channel}/{data['name']} уже есть")
    for field, value in data.items():
        setattr(target, field, value)
    db.commit()
    db.refresh(target)
    return TargetOut(
        id=target.id, channel=target.channel, name=target.name, description=target.description,
        config=target.config, enabled=target.enabled, created_at=target.created_at,
    )


@router.delete("/targets/{target_id}", status_code=204)
def delete_target(
    target_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> None:
    if db.get(ChannelTarget, target_id) is None:
        raise HTTPException(status_code=404, detail="Цель не найдена")
    db.execute(delete(ChannelTarget).where(ChannelTarget.id == target_id))
    # ссылки из routing_rule_actions/deliveries обнуляет FK SET NULL
    db.commit()


# --- rules ---

def _rule_to_out(db: Session, rule: RoutingRule) -> RuleOut:
    targets = {
        t.id: t.name
        for t in db.execute(select(ChannelTarget)).scalars().all()
    }
    actions = db.execute(
        select(RoutingRuleAction).where(RoutingRuleAction.rule_id == rule.id).order_by(RoutingRuleAction.id)
    ).scalars().all()
    return RuleOut(
        id=rule.id, name=rule.name, enabled=rule.enabled, weight=rule.weight,
        conditions=rule.conditions, template=rule.template,
        throttle_seconds=rule.throttle_seconds, created_at=rule.created_at,
        actions=[
            RuleActionOut(
                id=act.id, channel=act.channel, target_id=act.target_id,
                template=act.template, target_name=targets.get(act.target_id),
            )
            for act in actions
        ],
    )


@router.get("/rules", response_model=list[RuleOut])
def list_rules(
    user: AuthenticatedUser = Depends(require_admin), db: Session = Depends(get_db)
) -> list[RuleOut]:
    rules = db.execute(select(RoutingRule).order_by(RoutingRule.weight, RoutingRule.id)).scalars().all()
    return [_rule_to_out(db, r) for r in rules]


@router.post("/rules", response_model=RuleOut, status_code=201)
def create_rule(
    payload: RuleIn,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> RuleOut:
    for act in payload.actions:
        target = db.get(ChannelTarget, act.target_id) if act.target_id else "ok"
        if not target:
            raise HTTPException(status_code=422, detail=f"Цель id={act.target_id} не найдена")
        if act.channel in ("user", "push") and act.target_id:
            raise HTTPException(
                status_code=422,
                detail=f"Канал {act.channel} адресует пользователя через payload.user_id — цель не задаётся",
            )
    data = payload.model_dump()
    actions = data.pop("actions")
    rule = RoutingRule(**data)
    db.add(rule)
    db.flush()
    for act in actions:
        db.add(RoutingRuleAction(rule_id=rule.id, **act))
    db.commit()
    db.refresh(rule)
    return _rule_to_out(db, rule)


@router.patch("/rules/{rule_id}", response_model=RuleOut)
def patch_rule(
    rule_id: int,
    payload: RulePatch,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> RuleOut:
    rule = db.get(RoutingRule, rule_id)
    if rule is None:
        raise HTTPException(status_code=404, detail="Правило не найдено")
    data = payload.model_dump(exclude_unset=True)
    if "actions" in data:
        actions = data.pop("actions")
        for act in actions:
            target = db.get(ChannelTarget, act.get("target_id")) if act.get("target_id") else "ok"
            if not target:
                raise HTTPException(status_code=422, detail=f"Цель id={act['target_id']} не найдена")
            if act.get("channel") in ("user", "push") and act.get("target_id"):
                raise HTTPException(
                    status_code=422,
                    detail=f"Канал {act['channel']} адресует пользователя через payload.user_id — цель не задаётся",
                )
        db.execute(delete(RoutingRuleAction).where(RoutingRuleAction.rule_id == rule.id))
        for act in actions:
            db.add(RoutingRuleAction(rule_id=rule.id, **act))
    for field, value in data.items():
        setattr(rule, field, value)
    db.commit()
    db.refresh(rule)
    return _rule_to_out(db, rule)


@router.delete("/rules/{rule_id}", status_code=204)
def delete_rule(
    rule_id: int,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> None:
    if db.get(RoutingRule, rule_id) is None:
        raise HTTPException(status_code=404, detail="Правило не найдено")
    db.execute(delete(RoutingRuleAction).where(RoutingRuleAction.rule_id == rule_id))
    db.execute(delete(RoutingRule).where(RoutingRule.id == rule_id))
    db.commit()


# --- events / deliveries (чтение) ---

def _target_names(db: Session) -> dict[int, str]:
    return {t.id: t.name for t in db.execute(select(ChannelTarget)).scalars().all()}


def _deliveries_out(db: Session, event_ids) -> dict[uuid.UUID, list[DeliveryOut]]:
    if not event_ids:
        return {}
    names = _target_names(db)
    rows = (
        db.execute(select(Delivery).where(Delivery.event_id.in_(event_ids)).order_by(Delivery.id))
        .scalars()
        .all()
    )
    out: dict[uuid.UUID, list[DeliveryOut]] = {}
    for d in rows:
        out.setdefault(d.event_id, []).append(
            DeliveryOut(
                id=d.id, event_id=d.event_id, rule_id=d.rule_id, channel=d.channel,
                target_id=d.channel_target_id, target_name=names.get(d.channel_target_id),
                recipient_user_id=d.recipient_user_id,
                status=d.status, attempts=d.attempts, last_error=d.last_error,
                next_retry_at=d.next_retry_at, rendered_message=d.rendered_message,
                created_at=d.created_at, delivered_at=d.delivered_at,
            )
        )
    return out


@router.get("/events", response_model=list[AdminEventOut])
def list_events(
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
    limit: int = Query(50, ge=1, le=200),
    status_filter: str | None = Query(None, alias="status"),
    source_id: int | None = None,
) -> list[AdminEventOut]:
    q = select(Event).order_by(Event.created_at.desc()).limit(limit)
    if status_filter:
        q = q.where(Event.status == status_filter)
    if source_id:
        q = q.where(Event.source_id == source_id)
    events = db.execute(q).scalars().all()
    names = {s.id: s.name for s in db.execute(select(Source)).scalars().all()}
    group = _deliveries_out(db, [e.id for e in events])
    return [
        AdminEventOut(
            id=e.id, source_id=e.source_id, source_name=names.get(e.source_id, "?"),
            subject=e.subject, action=e.action, priority=e.priority, status=e.status,
            payload=dict(e.payload or {}), dedup_key=e.dedup_key, created_at=e.created_at,
            expires_at=e.expires_at, deliveries=group.get(e.id, []),
        )
        for e in events
    ]


@router.get("/events/{event_id}", response_model=AdminEventOut)
def get_event(
    event_id: uuid.UUID,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
) -> AdminEventOut:
    event = db.get(Event, event_id)
    if event is None:
        raise HTTPException(status_code=404, detail="Событие не найдено")
    source = db.get(Source, event.source_id)
    group = _deliveries_out(db, [event.id])
    return AdminEventOut(
        id=event.id, source_id=event.source_id, source_name=source.name if source else "?",
        subject=event.subject, action=event.action, priority=event.priority, status=event.status,
        payload=dict(event.payload or {}), dedup_key=event.dedup_key, created_at=event.created_at,
        expires_at=event.expires_at, deliveries=group.get(event.id, []),
    )


@router.get("/deliveries", response_model=list[DeliveryOut])
def list_deliveries(
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
    limit: int = Query(100, ge=1, le=500),
    status_filter: str | None = Query(None, alias="status"),
    channel: str | None = None,
) -> list[DeliveryOut]:
    q = select(Delivery).order_by(Delivery.created_at.desc(), Delivery.id.desc()).limit(limit)
    if status_filter:
        q = q.where(Delivery.status == status_filter)
    if channel:
        q = q.where(Delivery.channel == channel)
    rows = db.execute(q).scalars().all()
    names = _target_names(db)
    return [
        DeliveryOut(
            id=d.id, event_id=d.event_id, rule_id=d.rule_id, channel=d.channel,
            target_id=d.channel_target_id, target_name=names.get(d.channel_target_id),
            recipient_user_id=d.recipient_user_id,
            status=d.status, attempts=d.attempts, last_error=d.last_error,
            next_retry_at=d.next_retry_at, rendered_message=d.rendered_message,
            created_at=d.created_at, delivered_at=d.delivered_at,
        )
        for d in rows
    ]

# --- Настройки (оверрайды .env; секреты write-only, docs/06) ---

@router.get("/settings", response_model=list[SettingOut])
def list_settings(
    user: AuthenticatedUser = Depends(require_admin), db: Session = Depends(get_db)
):
    """Реестр редактируемых настроек: оверрайд из БД, дефолт из .env, маска секретов."""
    out: list[SettingOut] = []
    env_settings = get_settings()
    for defn in EDITABLE.values():
        raw = get_setting_row(db, defn.key)
        default = getattr(env_settings, defn.key, None)
        out.append(
            SettingOut(
                key=defn.key,
                section=defn.section,
                label=defn.label,
                kind=defn.kind,
                help=defn.help,
                options=list(defn.options),
                # secret — write-only: факт наличия в set, значение не возвращается
                value=None if (is_secret(defn) or raw is None or default is None) else raw,
                set=raw is not None or default is not None,
            )
        )
    return out


@router.put("/settings")
def put_settings(
    body: SettingsPut,
    user: AuthenticatedUser = Depends(require_admin),
    db: Session = Depends(get_db),
):
    """Оверрайды поверх .env: строка — установить, null — вернуть дефолт .env.

    Секрет — write-only: "" значит «не менять» (UI не отправляет пустое),
    null — очистить оверрайд. Значение хранится строкой; коэрсия — в
    settings_store.get_setting (int/bool по реестру).
    """
    unknown = [k for k in body.values if k not in EDITABLE]
    if unknown:
        raise HTTPException(
            status.HTTP_422_UNPROCESSABLE_ENTITY,
            f"Неизвестные ключи настроек: {', '.join(sorted(unknown))}",
        )
    updated: list[str] = []
    for key, value in body.values.items():
        defn = EDITABLE[key]
        if value is None:
            set_setting(db, key, None)
        elif is_secret(defn):
            if value == "":
                continue  # пустая строка = не менять
            set_setting(db, key, value)
        else:
            err = validate_setting(defn, value)
            if err is not None:
                raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, err)
            set_setting(db, key, value)
        updated.append(key)
    db.commit()
    return {"ok": True, "updated": updated}
