from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import func, or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.auth import admin_user, current_user
from app.db import get_db
from app.models import ChannelTag, Tag, User
from app.routes._common import owned_or_404
from app.sync.autotag import run_autotag_all

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


@router.get("")
def list_tags(user: User = Depends(current_user), db: Session = Depends(get_db)) -> list[dict]:
    rows = db.execute(
        select(Tag, func.count(ChannelTag.id))
        .outerjoin(ChannelTag, ChannelTag.tag_id == Tag.id)
        .where(or_(Tag.user_id.is_(None), Tag.user_id == user.id))
        .group_by(Tag.id)
        .order_by(Tag.category, Tag.name)
    ).all()
    return [
        {
            "id": tag.id,
            "name": tag.name,
            "color": tag.color,
            "category": tag.category,
            "system": tag.user_id is None,
            "channel_count": count,
        }
        for tag, count in rows
    ]


class TagCreateIn(BaseModel):
    name: str
    category: str = "other"
    color: str | None = None


@router.post("")
def create_tag(
    body: TagCreateIn,
    user: User = Depends(current_user),
    db: Session = Depends(get_db),
) -> dict:
    name = body.name.strip()
    if not name:
        raise HTTPException(status_code=400, detail="name is required")
    # An unknown category silently falls back to "other" (unchanged behaviour) rather than 422.
    category = body.category if body.category in ("language", "topic", "other") else "other"
    tag = Tag(
        user_id=user.id,
        name=name,
        color=body.color,
        category=category,
    )
    db.add(tag)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=409, detail=f"You already have a tag named “{name}”.")
    return {"id": tag.id, "name": tag.name, "color": tag.color, "category": tag.category}


def _own_tag(db: Session, user: User, tag_id: int) -> Tag:
    # System tags (user_id NULL) are not user-editable → owned_or_404 404s them too.
    return owned_or_404(db, Tag, tag_id, user, detail="Unknown tag")


class TagUpdateIn(BaseModel):
    # PATCH: presence matters, and `color: null` explicitly CLEARS it — so the endpoint reads
    # model_fields_set rather than testing for None (which couldn't tell absent from cleared).
    name: str | None = None
    color: str | None = None


@router.patch("/{tag_id}")
def update_tag(
    tag_id: int,
    body: TagUpdateIn,
    user: User = Depends(current_user),
    db: Session = Depends(get_db),
) -> dict:
    tag = _own_tag(db, user, tag_id)
    sent = body.model_fields_set
    if "name" in sent:
        name = (body.name or "").strip()
        if not name:
            raise HTTPException(status_code=400, detail="name cannot be empty")
        tag.name = name
    if "color" in sent:
        tag.color = body.color
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=409, detail=f"You already have a tag named “{tag.name}”.")
    return {"id": tag.id, "name": tag.name, "color": tag.color, "category": tag.category}


@router.delete("/{tag_id}")
def delete_tag(
    tag_id: int,
    user: User = Depends(current_user),
    db: Session = Depends(get_db),
) -> dict:
    tag = _own_tag(db, user, tag_id)
    db.delete(tag)  # ChannelTag rows cascade on tag delete.
    db.commit()
    return {"deleted": tag_id}


@router.post("/recompute")
def recompute(
    _: User = Depends(admin_user),
    db: Session = Depends(get_db),
    only_missing: bool = False,
) -> dict:
    return run_autotag_all(db, only_missing=only_missing)
