"""Public templates gallery endpoints. No authentication required."""
import logging
from typing import Annotated, Any
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.core.database import get_db
from app.core.rate_limit import limiter
from app.models.category import TreeCategory
from app.models.script_template import ScriptCategory, ScriptTemplate
from app.models.tag import TreeTag
from app.models.tree import Tree
from app.schemas.public_templates import (
    PublicFlowDetail,
    PublicFlowTemplate,
    PublicGalleryResponse,
    PublicScriptDetail,
    PublicScriptTemplate,
)

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/public/templates", tags=["public-gallery"])


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _count_tree_steps(structure: dict[str, Any] | None) -> int:
    """Count nodes in a tree structure via BFS."""
    if not structure:
        return 0
    count = 0
    queue = [structure]
    while queue:
        node = queue.pop()
        count += 1
        for child in node.get("children", []):
            queue.append(child)
        for option in node.get("options", []):
            if isinstance(option, dict) and "children" in option:
                for child in option["children"]:
                    queue.append(child)
    return count


def _truncate_structure(structure: dict[str, Any] | None, max_depth: int = 3) -> dict[str, Any] | None:
    """Return a copy of the tree structure truncated to max_depth levels."""
    if not structure:
        return None

    def _truncate_node(node: dict[str, Any], depth: int) -> dict[str, Any]:
        if depth >= max_depth:
            # Return node skeleton without children
            return {k: v for k, v in node.items() if k not in ("children", "options")}
        result = {k: v for k, v in node.items() if k not in ("children", "options")}
        if "children" in node:
            result["children"] = [_truncate_node(c, depth + 1) for c in node["children"]]
        if "options" in node:
            truncated_options = []
            for opt in node["options"]:
                if isinstance(opt, dict):
                    opt_copy = {k: v for k, v in opt.items() if k != "children"}
                    if "children" in opt:
                        opt_copy["children"] = [_truncate_node(c, depth + 1) for c in opt["children"]]
                    truncated_options.append(opt_copy)
                else:
                    truncated_options.append(opt)
            result["options"] = truncated_options
        return result

    return _truncate_node(structure, 0)


def _build_flow_template(tree: Tree) -> PublicFlowTemplate:
    category_name = None
    if tree.category_rel:
        category_name = tree.category_rel.name
    elif tree.category:
        category_name = tree.category

    tag_names = [tag.name for tag in (tree.tags or [])]

    return PublicFlowTemplate(
        id=tree.id,
        name=tree.name,
        description=tree.description,
        category=category_name,
        tree_type=tree.tree_type,
        step_count=_count_tree_steps(tree.tree_structure),
        usage_count=tree.usage_count,
        success_rate=tree.success_rate,
        tags=tag_names,
        preview_structure=_truncate_structure(tree.tree_structure),
        created_at=tree.created_at,
    )


def _build_script_template(script: ScriptTemplate) -> PublicScriptTemplate:
    param_count = 0
    if isinstance(script.parameters_schema, dict):
        params = script.parameters_schema.get("parameters", script.parameters_schema.get("properties", {}))
        if isinstance(params, list):
            param_count = len(params)
        elif isinstance(params, dict):
            param_count = len(params)

    return PublicScriptTemplate(
        id=script.id,
        name=script.name,
        description=script.description,
        category_name=script.category.name if script.category else None,
        category_icon=script.category.icon if script.category else None,
        complexity=script.complexity,
        tags=list(script.tags) if script.tags else [],
        parameter_count=param_count,
        requires_elevation=script.requires_elevation,
        requires_modules=list(script.requires_modules) if script.requires_modules else [],
        usage_count=script.usage_count,
        is_verified=script.is_verified,
        created_at=script.created_at,
    )


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------

@router.get("", response_model=PublicGalleryResponse)
@limiter.limit("30/minute")
async def list_gallery(
    request: Request,
    db: Annotated[AsyncSession, Depends(get_db)],
    category: str | None = Query(None, description="Filter by category name"),
    type: str = Query("all", description="Filter type: flows, scripts, or all"),
    sort: str = Query("usage", description="Sort order: usage, newest, success_rate"),
    page: int = Query(1, ge=1),
    per_page: int = Query(20, ge=1, le=100),
):
    """Public gallery listing. Returns featured flows and scripts.

    No authentication required. Rate limited to 30/minute.
    """
    offset = (page - 1) * per_page

    flow_templates: list[PublicFlowTemplate] = []
    script_templates: list[PublicScriptTemplate] = []
    total_flows = 0
    total_scripts = 0
    category_names: set[str] = set()
    domains: list[str] = []

    # --- Flow templates ---
    if type in ("all", "flows"):
        q = (
            select(Tree)
            .options(selectinload(Tree.category_rel), selectinload(Tree.tags))
            .where(Tree.is_gallery_featured == True)  # noqa: E712
            .where(Tree.is_active == True)  # noqa: E712
            .where(Tree.deleted_at == None)  # noqa: E711
        )

        if category:
            q = q.join(TreeCategory, Tree.category_id == TreeCategory.id, isouter=True).where(
                or_(TreeCategory.name == category, Tree.category == category)
            )

        # Count query
        count_q = select(func.count()).select_from(q.subquery())
        total_flows = (await db.execute(count_q)).scalar_one()

        # Sort
        if sort == "newest":
            q = q.order_by(Tree.created_at.desc())
        elif sort == "success_rate":
            q = q.order_by(Tree.success_rate.desc().nulls_last(), Tree.usage_count.desc())
        else:  # usage (default)
            q = q.order_by(Tree.usage_count.desc(), Tree.gallery_sort_order.asc())

        q = q.offset(offset).limit(per_page)
        result = await db.execute(q)
        trees = result.scalars().all()

        for tree in trees:
            flow_templates.append(_build_flow_template(tree))
            cat = None
            if tree.category_rel:
                cat = tree.category_rel.name
            elif tree.category:
                cat = tree.category
            if cat:
                category_names.add(cat)

    # --- Script templates ---
    if type in ("all", "scripts"):
        sq = (
            select(ScriptTemplate)
            .options(selectinload(ScriptTemplate.category))
            .where(ScriptTemplate.is_gallery_featured == True)  # noqa: E712
            .where(ScriptTemplate.is_active == True)  # noqa: E712
        )

        if category:
            sq = sq.join(ScriptCategory, ScriptTemplate.category_id == ScriptCategory.id).where(
                ScriptCategory.name == category
            )

        count_sq = select(func.count()).select_from(sq.subquery())
        total_scripts = (await db.execute(count_sq)).scalar_one()

        if sort == "newest":
            sq = sq.order_by(ScriptTemplate.created_at.desc())
        elif sort == "success_rate":
            sq = sq.order_by(ScriptTemplate.usage_count.desc())
        else:
            sq = sq.order_by(ScriptTemplate.usage_count.desc(), ScriptTemplate.gallery_sort_order.asc())

        sq = sq.offset(offset).limit(per_page)
        result = await db.execute(sq)
        scripts = result.scalars().all()

        for script in scripts:
            script_templates.append(_build_script_template(script))
            if script.category:
                category_names.add(script.category.name)

    return PublicGalleryResponse(
        flow_templates=flow_templates,
        script_templates=script_templates,
        total_flows=total_flows,
        total_scripts=total_scripts,
        categories=sorted(category_names),
        domains=domains,
    )


@router.get("/flows/{flow_id}", response_model=PublicFlowDetail)
@limiter.limit("30/minute")
async def get_flow_detail(
    request: Request,
    flow_id: UUID,
    db: Annotated[AsyncSession, Depends(get_db)],
):
    """Get a single featured flow template (preview only — tree truncated to 3 levels).

    No authentication required. Script body is never exposed.
    """
    result = await db.execute(
        select(Tree)
        .options(selectinload(Tree.category_rel), selectinload(Tree.tags))
        .where(Tree.id == flow_id)
        .where(Tree.is_gallery_featured == True)  # noqa: E712
        .where(Tree.is_active == True)  # noqa: E712
        .where(Tree.deleted_at == None)  # noqa: E711
    )
    tree = result.scalar_one_or_none()
    if not tree:
        raise HTTPException(status_code=404, detail="Flow template not found")

    return _build_flow_template(tree)


@router.get("/scripts/{script_id}", response_model=PublicScriptDetail)
@limiter.limit("30/minute")
async def get_script_detail(
    request: Request,
    script_id: UUID,
    db: Annotated[AsyncSession, Depends(get_db)],
):
    """Get a single featured script template detail.

    NOTE: script_body is NEVER returned — it is behind the signup wall.
    Only parameter names/descriptions are included.
    """
    result = await db.execute(
        select(ScriptTemplate)
        .options(selectinload(ScriptTemplate.category))
        .where(ScriptTemplate.id == script_id)
        .where(ScriptTemplate.is_gallery_featured == True)  # noqa: E712
        .where(ScriptTemplate.is_active == True)  # noqa: E712
    )
    script = result.scalar_one_or_none()
    if not script:
        raise HTTPException(status_code=404, detail="Script template not found")

    # Build safe parameter list (no script_body — that stays locked behind auth)
    safe_params: list[dict[str, Any]] = []
    schema = script.parameters_schema or {}
    raw_params = schema.get("parameters", schema.get("properties", {}))
    if isinstance(raw_params, list):
        for p in raw_params:
            if isinstance(p, dict):
                safe_params.append({
                    "name": p.get("name", ""),
                    "description": p.get("description", ""),
                    "type": p.get("type", "string"),
                    "required": p.get("required", False),
                    "default": p.get("default"),
                })
    elif isinstance(raw_params, dict):
        for param_name, param_def in raw_params.items():
            if isinstance(param_def, dict):
                safe_params.append({
                    "name": param_name,
                    "description": param_def.get("description", ""),
                    "type": param_def.get("type", "string"),
                    "required": param_def.get("required", False),
                    "default": param_def.get("default"),
                })

    return PublicScriptDetail(
        id=script.id,
        name=script.name,
        description=script.description,
        category_name=script.category.name if script.category else None,
        complexity=script.complexity,
        tags=list(script.tags) if script.tags else [],
        parameters=safe_params,
        requires_elevation=script.requires_elevation,
        requires_modules=list(script.requires_modules) if script.requires_modules else [],
        usage_count=script.usage_count,
        is_verified=script.is_verified,
        created_at=script.created_at,
    )


@router.get("/categories")
@limiter.limit("30/minute")
async def list_categories(
    request: Request,
    db: Annotated[AsyncSession, Depends(get_db)],
):
    """Return categories that have at least one gallery-featured item, with counts.

    No authentication required.
    """
    # Flow categories
    flow_cat_result = await db.execute(
        select(TreeCategory.name, func.count(Tree.id).label("flow_count"))
        .join(Tree, Tree.category_id == TreeCategory.id)
        .where(Tree.is_gallery_featured == True)  # noqa: E712
        .where(Tree.is_active == True)  # noqa: E712
        .where(Tree.deleted_at == None)  # noqa: E711
        .group_by(TreeCategory.name)
        .order_by(TreeCategory.name)
    )
    flow_cats = flow_cat_result.all()

    # Script categories
    script_cat_result = await db.execute(
        select(ScriptCategory.name, func.count(ScriptTemplate.id).label("script_count"))
        .join(ScriptTemplate, ScriptTemplate.category_id == ScriptCategory.id)
        .where(ScriptTemplate.is_gallery_featured == True)  # noqa: E712
        .where(ScriptTemplate.is_active == True)  # noqa: E712
        .group_by(ScriptCategory.name)
        .order_by(ScriptCategory.name)
    )
    script_cats = script_cat_result.all()

    categories = []
    seen: set[str] = set()

    for name, flow_count in flow_cats:
        if name not in seen:
            categories.append({"name": name, "flow_count": flow_count, "script_count": 0})
            seen.add(name)

    for name, script_count in script_cats:
        if name in seen:
            for cat in categories:
                if cat["name"] == name:
                    cat["script_count"] = script_count
                    break
        else:
            categories.append({"name": name, "flow_count": 0, "script_count": script_count})
            seen.add(name)

    return {"categories": categories}


@router.get("/search")
@limiter.limit("30/minute")
async def search_gallery(
    request: Request,
    db: Annotated[AsyncSession, Depends(get_db)],
    q: str = Query(..., min_length=1, max_length=200, description="Search query"),
    type: str = Query("all", description="Filter type: flows, scripts, or all"),
    page: int = Query(1, ge=1),
    per_page: int = Query(20, ge=1, le=100),
):
    """Full-text search across featured gallery items.

    Searches name, description, and tags. No authentication required.
    """
    offset = (page - 1) * per_page
    search_term = f"%{q.lower()}%"

    flow_templates: list[PublicFlowTemplate] = []
    script_templates: list[PublicScriptTemplate] = []
    total_flows = 0
    total_scripts = 0

    if type in ("all", "flows"):
        flow_q = (
            select(Tree)
            .options(selectinload(Tree.category_rel), selectinload(Tree.tags))
            .where(Tree.is_gallery_featured == True)  # noqa: E712
            .where(Tree.is_active == True)  # noqa: E712
            .where(Tree.deleted_at == None)  # noqa: E711
            .where(
                or_(
                    func.lower(Tree.name).like(search_term),
                    func.lower(Tree.description).like(search_term),
                    func.lower(Tree.category).like(search_term),
                )
            )
            .order_by(Tree.usage_count.desc())
        )
        count_q = select(func.count()).select_from(flow_q.subquery())
        total_flows = (await db.execute(count_q)).scalar_one()

        result = await db.execute(flow_q.offset(offset).limit(per_page))
        for tree in result.scalars().all():
            flow_templates.append(_build_flow_template(tree))

    if type in ("all", "scripts"):
        script_q = (
            select(ScriptTemplate)
            .options(selectinload(ScriptTemplate.category))
            .where(ScriptTemplate.is_gallery_featured == True)  # noqa: E712
            .where(ScriptTemplate.is_active == True)  # noqa: E712
            .where(
                or_(
                    func.lower(ScriptTemplate.name).like(search_term),
                    func.lower(ScriptTemplate.description).like(search_term),
                )
            )
            .order_by(ScriptTemplate.usage_count.desc())
        )
        count_sq = select(func.count()).select_from(script_q.subquery())
        total_scripts = (await db.execute(count_sq)).scalar_one()

        result = await db.execute(script_q.offset(offset).limit(per_page))
        for script in result.scalars().all():
            script_templates.append(_build_script_template(script))

    return {
        "flow_templates": flow_templates,
        "script_templates": script_templates,
        "total_flows": total_flows,
        "total_scripts": total_scripts,
        "query": q,
    }
