"""Taxonomy API — folder and tag CRUD, sidebar data."""

from __future__ import annotations

import uuid
from datetime import datetime
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func, update, delete
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_user_id, get_db_session
from app.models.taxonomy import Folder, Tag, ItemTag, slugify, ensure_user_taxonomy
from app.models.item import Item

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


# ── Schemas ──

class FolderIn(BaseModel):
    name: str

class FolderOut(BaseModel):
    id: str
    name: str
    slug: str
    is_active: bool
    sort_order: int
    item_count: int = 0
    model_config = {"from_attributes": True}

class TagIn(BaseModel):
    name: str

class TagOut(BaseModel):
    id: str
    name: str
    slug: str
    is_active: bool
    sort_order: int
    item_count: int = 0
    model_config = {"from_attributes": True}

class SidebarOut(BaseModel):
    folders: list[FolderOut]
    tags: list[TagOut]
    total_items: int

class MergeTagIn(BaseModel):
    source_tag_id: str
    target_tag_id: str


# ── Sidebar data ──

@router.get("/sidebar", response_model=SidebarOut)
async def get_sidebar(
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    await ensure_user_taxonomy(db, user_id)

    # Folders with counts
    folder_rows = (await db.execute(
        select(Folder).where(Folder.user_id == user_id).order_by(Folder.sort_order, Folder.name)
    )).scalars().all()

    folder_counts = {}
    for row in (await db.execute(
        select(Item.folder_id, func.count()).where(
            Item.user_id == user_id, Item.folder_id.isnot(None)
        ).group_by(Item.folder_id)
    )).all():
        folder_counts[row[0]] = row[1]

    folders = [FolderOut(
        id=f.id, name=f.name, slug=f.slug, is_active=f.is_active,
        sort_order=f.sort_order, item_count=folder_counts.get(f.id, 0)
    ) for f in folder_rows]

    # Tags with counts
    tag_rows = (await db.execute(
        select(Tag).where(Tag.user_id == user_id).order_by(Tag.sort_order, Tag.name)
    )).scalars().all()

    tag_counts = {}
    for row in (await db.execute(
        select(ItemTag.tag_id, func.count()).join(Item, Item.id == ItemTag.item_id).where(
            Item.user_id == user_id
        ).group_by(ItemTag.tag_id)
    )).all():
        tag_counts[row[0]] = row[1]

    tags = [TagOut(
        id=t.id, name=t.name, slug=t.slug, is_active=t.is_active,
        sort_order=t.sort_order, item_count=tag_counts.get(t.id, 0)
    ) for t in tag_rows]

    # Total items
    total = (await db.execute(
        select(func.count()).where(Item.user_id == user_id)
    )).scalar() or 0

    return SidebarOut(folders=folders, tags=tags, total_items=total)


# ── Folder CRUD ──

@router.get("/folders", response_model=list[FolderOut])
async def list_folders(
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    await ensure_user_taxonomy(db, user_id)
    rows = (await db.execute(
        select(Folder).where(Folder.user_id == user_id).order_by(Folder.sort_order, Folder.name)
    )).scalars().all()
    return [FolderOut(id=f.id, name=f.name, slug=f.slug, is_active=f.is_active, sort_order=f.sort_order) for f in rows]


@router.post("/folders", response_model=FolderOut, status_code=201)
async def create_folder(
    body: FolderIn,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    slug = slugify(body.name)
    existing = (await db.execute(
        select(Folder).where(Folder.user_id == user_id, Folder.slug == slug)
    )).scalar_one_or_none()
    if existing:
        raise HTTPException(400, "Folder already exists")

    max_order = (await db.execute(
        select(func.max(Folder.sort_order)).where(Folder.user_id == user_id)
    )).scalar() or 0

    folder = Folder(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug, sort_order=max_order + 1)
    db.add(folder)
    await db.commit()
    return FolderOut(id=folder.id, name=folder.name, slug=folder.slug, is_active=folder.is_active, sort_order=folder.sort_order)


@router.patch("/folders/{folder_id}", response_model=FolderOut)
async def update_folder(
    folder_id: str,
    body: FolderIn,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    folder = (await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == user_id)
    )).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, "Folder not found")

    folder.name = body.name.strip()
    folder.slug = slugify(body.name)
    folder.updated_at = datetime.utcnow()

    # Update denormalized folder name on items
    await db.execute(
        update(Item).where(Item.folder_id == folder_id).values(folder=folder.name)
    )
    await db.commit()
    return FolderOut(id=folder.id, name=folder.name, slug=folder.slug, is_active=folder.is_active, sort_order=folder.sort_order)


@router.delete("/folders/{folder_id}")
async def delete_folder(
    folder_id: str,
    fallback_folder_id: Optional[str] = Query(None),
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    folder = (await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == user_id)
    )).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, "Folder not found")

    # Move items to fallback folder or first available folder
    if fallback_folder_id:
        fallback = (await db.execute(select(Folder).where(Folder.id == fallback_folder_id))).scalar_one_or_none()
    else:
        fallback = (await db.execute(
            select(Folder).where(Folder.user_id == user_id, Folder.id != folder_id).order_by(Folder.sort_order).limit(1)
        )).scalar_one_or_none()

    if fallback:
        await db.execute(
            update(Item).where(Item.folder_id == folder_id).values(folder_id=fallback.id, folder=fallback.name)
        )

    await db.execute(delete(Folder).where(Folder.id == folder_id))
    await db.commit()
    return {"status": "deleted", "items_moved_to": fallback.name if fallback else None}


# ── Tag CRUD ──

@router.get("/tags", response_model=list[TagOut])
async def list_tags(
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    await ensure_user_taxonomy(db, user_id)
    rows = (await db.execute(
        select(Tag).where(Tag.user_id == user_id).order_by(Tag.sort_order, Tag.name)
    )).scalars().all()
    return [TagOut(id=t.id, name=t.name, slug=t.slug, is_active=t.is_active, sort_order=t.sort_order) for t in rows]


@router.post("/tags", response_model=TagOut, status_code=201)
async def create_tag(
    body: TagIn,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    slug = slugify(body.name)
    existing = (await db.execute(
        select(Tag).where(Tag.user_id == user_id, Tag.slug == slug)
    )).scalar_one_or_none()
    if existing:
        raise HTTPException(400, "Tag already exists")

    max_order = (await db.execute(
        select(func.max(Tag.sort_order)).where(Tag.user_id == user_id)
    )).scalar() or 0

    tag = Tag(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug, sort_order=max_order + 1)
    db.add(tag)
    await db.commit()
    return TagOut(id=tag.id, name=tag.name, slug=tag.slug, is_active=tag.is_active, sort_order=tag.sort_order)


@router.patch("/tags/{tag_id}", response_model=TagOut)
async def update_tag(
    tag_id: str,
    body: TagIn,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    tag = (await db.execute(
        select(Tag).where(Tag.id == tag_id, Tag.user_id == user_id)
    )).scalar_one_or_none()
    if not tag:
        raise HTTPException(404, "Tag not found")

    old_name = tag.name
    tag.name = body.name.strip()
    tag.slug = slugify(body.name)
    tag.updated_at = datetime.utcnow()

    # Update denormalized tags array on items that had the old name
    items_with_tag = (await db.execute(
        select(Item).join(ItemTag, ItemTag.item_id == Item.id).where(ItemTag.tag_id == tag_id)
    )).scalars().all()
    for item in items_with_tag:
        if item.tags and old_name in item.tags:
            item.tags = [tag.name if t == old_name else t for t in item.tags]

    await db.commit()
    return TagOut(id=tag.id, name=tag.name, slug=tag.slug, is_active=tag.is_active, sort_order=tag.sort_order)


@router.delete("/tags/{tag_id}")
async def delete_tag(
    tag_id: str,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    tag = (await db.execute(
        select(Tag).where(Tag.id == tag_id, Tag.user_id == user_id)
    )).scalar_one_or_none()
    if not tag:
        raise HTTPException(404, "Tag not found")

    # Remove tag from denormalized arrays
    items_with_tag = (await db.execute(
        select(Item).join(ItemTag, ItemTag.item_id == Item.id).where(ItemTag.tag_id == tag_id)
    )).scalars().all()
    for item in items_with_tag:
        if item.tags and tag.name in item.tags:
            item.tags = [t for t in item.tags if t != tag.name]

    # Delete join table entries and tag
    await db.execute(delete(ItemTag).where(ItemTag.tag_id == tag_id))
    await db.execute(delete(Tag).where(Tag.id == tag_id))
    await db.commit()
    return {"status": "deleted"}


@router.post("/tags/merge")
async def merge_tags(
    body: MergeTagIn,
    user_id: str = Depends(get_user_id),
    db: AsyncSession = Depends(get_db_session),
):
    """Merge source tag into target tag. All items with source get target instead."""
    source = (await db.execute(select(Tag).where(Tag.id == body.source_tag_id, Tag.user_id == user_id))).scalar_one_or_none()
    target = (await db.execute(select(Tag).where(Tag.id == body.target_tag_id, Tag.user_id == user_id))).scalar_one_or_none()
    if not source or not target:
        raise HTTPException(404, "Tag not found")

    # Move item_tags from source to target (skip duplicates)
    source_items = (await db.execute(
        select(ItemTag.item_id).where(ItemTag.tag_id == source.id)
    )).scalars().all()
    target_items = set((await db.execute(
        select(ItemTag.item_id).where(ItemTag.tag_id == target.id)
    )).scalars().all())

    for item_id in source_items:
        if item_id not in target_items:
            db.add(ItemTag(item_id=item_id, tag_id=target.id))

    # Update denormalized tags
    items = (await db.execute(
        select(Item).join(ItemTag, ItemTag.item_id == Item.id).where(ItemTag.tag_id == source.id)
    )).scalars().all()
    for item in items:
        if item.tags:
            new_tags = [target.name if t == source.name else t for t in item.tags]
            item.tags = list(dict.fromkeys(new_tags))  # dedupe

    # Delete source
    await db.execute(delete(ItemTag).where(ItemTag.tag_id == source.id))
    await db.execute(delete(Tag).where(Tag.id == source.id))
    await db.commit()
    return {"status": "merged", "source": source.name, "target": target.name}
