"""Technique page endpoints — list and detail with eager-loaded relations."""

from __future__ import annotations

import json
import logging
from typing import Annotated

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

from database import get_session
from models import Creator, KeyMoment, RelatedTechniqueLink, SourceVideo, TechniquePage, TechniquePageVersion, TechniquePageVideo
from redis_client import get_redis
from schemas import (
    CreatorInfo,
    KeyMomentSummary,
    PaginatedResponse,
    RelatedLinkItem,
    SourceVideoSummary,
    TechniquePageDetail,
    TechniquePageRead,
    TechniquePageVersionDetail,
    TechniquePageVersionListResponse,
    TechniquePageVersionSummary,
)

logger = logging.getLogger("chrysopedia.techniques")

RELATED_CACHE_TTL = 3600  # 1 hour — related links only change when pages are created

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


async def _find_dynamic_related(
    db: AsyncSession,
    page: TechniquePage,
    exclude_slugs: set[str],
    limit: int,
) -> list[RelatedLinkItem]:
    """Score and return dynamically related technique pages.

    Results are cached in Redis for 1 hour per page slug since related
    links only change when technique pages are created or updated.

    Scoring:
      - Same creator + same topic_category: +3
      - Same creator, different category: +2
      - Same topic_category, different creator: +2
      - Each overlapping topic_tag: +1
    """
    cache_key = f"chrysopedia:related:{page.slug}"

    # Try Redis cache
    try:
        redis = await get_redis()
        cached = await redis.get(cache_key)
        await redis.aclose()
        if cached:
            items = json.loads(cached)
            # Filter out any that should be excluded and respect limit
            filtered = [i for i in items if i["target_slug"] not in exclude_slugs]
            return [RelatedLinkItem(**i) for i in filtered[:limit]]
    except Exception:
        pass

    # Cache miss — compute from DB
    stmt = (
        select(TechniquePage)
        .options(selectinload(TechniquePage.creator))
        .where(TechniquePage.id != page.id)
    )
    if exclude_slugs:
        stmt = stmt.where(TechniquePage.slug.notin_(exclude_slugs))

    result = await db.execute(stmt)
    candidates = result.scalars().all()

    if not candidates:
        return []

    current_tags = set(page.topic_tags) if page.topic_tags else set()

    scored: list[tuple[int, str, TechniquePage]] = []
    for cand in candidates:
        score = 0
        reasons: list[str] = []

        same_creator = cand.creator_id == page.creator_id
        same_category = cand.topic_category == page.topic_category

        if same_creator and same_category:
            score += 3
            reasons.append("Same creator, same topic")
        elif same_creator:
            score += 2
            reasons.append("Same creator")
        elif same_category:
            score += 2
            reasons.append(f"Also about {page.topic_category}")

        if current_tags:
            cand_tags = set(cand.topic_tags) if cand.topic_tags else set()
            shared = current_tags & cand_tags
            if shared:
                score += len(shared)
                reasons.append(f"Shared tags: {', '.join(sorted(shared))}")

        if score > 0:
            scored.append((score, "; ".join(reasons), cand))

    scored.sort(key=lambda x: (-x[0], x[2].title))

    results: list[RelatedLinkItem] = []
    for score, reason, cand in scored[:limit]:
        creator_name = cand.creator.name if cand.creator else ""
        results.append(
            RelatedLinkItem(
                target_title=cand.title,
                target_slug=cand.slug,
                relationship="dynamic",
                creator_name=creator_name,
                topic_category=cand.topic_category,
                reason=reason,
            )
        )

    # Write to Redis cache (best-effort)
    try:
        redis = await get_redis()
        await redis.set(
            cache_key,
            json.dumps([r.model_dump() for r in results]),
            ex=RELATED_CACHE_TTL,
        )
        await redis.aclose()
    except Exception:
        pass

    return results


@router.get("", response_model=PaginatedResponse)
async def list_techniques(
    category: Annotated[str | None, Query()] = None,
    creator_slug: Annotated[str | None, Query()] = None,
    sort: Annotated[str, Query()] = "recent",
    offset: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 50,
    db: AsyncSession = Depends(get_session),
) -> PaginatedResponse:
    """List technique pages with optional category/creator filtering."""
    # Correlated subquery for key moment count (same pattern as creators.py)
    key_moment_count_sq = (
        select(func.count())
        .where(KeyMoment.technique_page_id == TechniquePage.id)
        .correlate(TechniquePage)
        .scalar_subquery()
    )

    # Build base query with filters
    base_stmt = select(TechniquePage.id)
    if category:
        base_stmt = base_stmt.where(TechniquePage.topic_category == category)
    if creator_slug:
        base_stmt = base_stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
            Creator.slug == creator_slug
        )

    # Count total before pagination
    count_stmt = select(func.count()).select_from(base_stmt.subquery())
    count_result = await db.execute(count_stmt)
    total = count_result.scalar() or 0

    # Main query with subquery column
    stmt = select(
        TechniquePage,
        key_moment_count_sq.label("key_moment_count"),
    )
    if category:
        stmt = stmt.where(TechniquePage.topic_category == category)
    if creator_slug:
        stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
            Creator.slug == creator_slug
        )

    stmt = stmt.options(selectinload(TechniquePage.creator))
    if sort == "random":
        stmt = stmt.order_by(func.random())
    elif sort == "oldest":
        stmt = stmt.order_by(TechniquePage.created_at.asc())
    elif sort == "alpha":
        stmt = stmt.order_by(TechniquePage.title.asc())
    elif sort == "creator":
        # Need a join for creator name ordering; avoid duplicate join if creator_slug filter already joined
        if not creator_slug:
            stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id, isouter=True)
        stmt = stmt.order_by(Creator.name.asc(), TechniquePage.title.asc())
    else:
        # Default: "recent" — newest first
        stmt = stmt.order_by(TechniquePage.created_at.desc())
    stmt = stmt.offset(offset).limit(limit)
    result = await db.execute(stmt)
    rows = result.all()

    items = []
    for row in rows:
        p = row[0]
        km_count = row[1] or 0
        item = TechniquePageRead.model_validate(p)
        if p.creator:
            item.creator_name = p.creator.name
            item.creator_slug = p.creator.slug
        item.key_moment_count = km_count
        items.append(item)

    return PaginatedResponse(
        items=items,
        total=total,
        offset=offset,
        limit=limit,
    )


@router.get("/random")
async def random_technique(
    db: AsyncSession = Depends(get_session),
) -> dict:
    """Return the slug of a single random technique page."""
    stmt = select(TechniquePage.slug).order_by(func.random()).limit(1)
    result = await db.execute(stmt)
    slug = result.scalar_one_or_none()
    if slug is None:
        raise HTTPException(status_code=404, detail="No techniques available")
    return {"slug": slug}


@router.get("/{slug}", response_model=TechniquePageDetail)
async def get_technique(
    slug: str,
    db: AsyncSession = Depends(get_session),
) -> TechniquePageDetail:
    """Get full technique page detail with key moments, creator, and related links."""
    stmt = (
        select(TechniquePage)
        .where(TechniquePage.slug == slug)
        .options(
            selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video),
            selectinload(TechniquePage.creator),
            selectinload(TechniquePage.outgoing_links).selectinload(
                RelatedTechniqueLink.target_page
            ),
            selectinload(TechniquePage.incoming_links).selectinload(
                RelatedTechniqueLink.source_page
            ),
            selectinload(TechniquePage.source_video_links).selectinload(
                TechniquePageVideo.source_video
            ),
        )
    )
    result = await db.execute(stmt)
    page = result.scalar_one_or_none()

    if page is None:
        raise HTTPException(status_code=404, detail=f"Technique '{slug}' not found")

    # Build key moments (ordered by start_time)
    key_moments = sorted(page.key_moments, key=lambda km: km.start_time)
    key_moment_items = []
    for km in key_moments:
        item = KeyMomentSummary.model_validate(km)
        item.video_filename = km.source_video.filename if km.source_video else ""
        key_moment_items.append(item)

    # Build creator info
    creator_info = None
    if page.creator:
        creator_info = CreatorInfo(
            name=page.creator.name,
            slug=page.creator.slug,
            genres=page.creator.genres,
        )

    # Build related links (outgoing + incoming)
    related_links: list[RelatedLinkItem] = []
    for link in page.outgoing_links:
        if link.target_page:
            related_links.append(
                RelatedLinkItem(
                    target_title=link.target_page.title,
                    target_slug=link.target_page.slug,
                    relationship=link.relationship.value if hasattr(link.relationship, 'value') else str(link.relationship),
                )
            )
    for link in page.incoming_links:
        if link.source_page:
            related_links.append(
                RelatedLinkItem(
                    target_title=link.source_page.title,
                    target_slug=link.source_page.slug,
                    relationship=link.relationship.value if hasattr(link.relationship, 'value') else str(link.relationship),
                )
            )

    # Supplement with dynamic related techniques (up to 4 total)
    curated_slugs = {link.target_slug for link in related_links}
    max_related = 4
    if len(related_links) < max_related:
        remaining = max_related - len(related_links)
        try:
            dynamic_links = await _find_dynamic_related(
                db, page, curated_slugs, remaining
            )
            related_links.extend(dynamic_links)
        except Exception:
            logger.warning(
                "Dynamic related query failed for %s, continuing with curated only",
                slug,
                exc_info=True,
            )

    base = TechniquePageRead.model_validate(page)

    # Count versions for this page
    version_count_stmt = select(func.count()).where(
        TechniquePageVersion.technique_page_id == page.id
    )
    version_count_result = await db.execute(version_count_stmt)
    version_count = version_count_result.scalar() or 0

    # Build source video list from association table
    source_videos = [
        SourceVideoSummary(
            id=link.source_video.id,
            filename=link.source_video.filename,
            content_type=link.source_video.content_type.value if hasattr(link.source_video.content_type, 'value') else str(link.source_video.content_type),
            added_at=link.added_at,
        )
        for link in page.source_video_links
        if link.source_video is not None
    ]

    return TechniquePageDetail(
        **base.model_dump(),
        key_moments=key_moment_items,
        creator_info=creator_info,
        related_links=related_links,
        version_count=version_count,
        source_videos=source_videos,
    )


@router.get("/{slug}/versions", response_model=TechniquePageVersionListResponse)
async def list_technique_versions(
    slug: str,
    db: AsyncSession = Depends(get_session),
) -> TechniquePageVersionListResponse:
    """List all version snapshots for a technique page, newest first."""
    # Resolve the technique page
    page_stmt = select(TechniquePage).where(TechniquePage.slug == slug)
    page_result = await db.execute(page_stmt)
    page = page_result.scalar_one_or_none()
    if page is None:
        raise HTTPException(status_code=404, detail=f"Technique '{slug}' not found")

    # Fetch versions ordered by version_number DESC
    versions_stmt = (
        select(TechniquePageVersion)
        .where(TechniquePageVersion.technique_page_id == page.id)
        .order_by(TechniquePageVersion.version_number.desc())
    )
    versions_result = await db.execute(versions_stmt)
    versions = versions_result.scalars().all()

    items = [TechniquePageVersionSummary.model_validate(v) for v in versions]
    return TechniquePageVersionListResponse(items=items, total=len(items))


@router.get("/{slug}/versions/{version_number}", response_model=TechniquePageVersionDetail)
async def get_technique_version(
    slug: str,
    version_number: int,
    db: AsyncSession = Depends(get_session),
) -> TechniquePageVersionDetail:
    """Get a specific version snapshot by version number."""
    # Resolve the technique page
    page_stmt = select(TechniquePage).where(TechniquePage.slug == slug)
    page_result = await db.execute(page_stmt)
    page = page_result.scalar_one_or_none()
    if page is None:
        raise HTTPException(status_code=404, detail=f"Technique '{slug}' not found")

    # Fetch the specific version
    version_stmt = (
        select(TechniquePageVersion)
        .where(
            TechniquePageVersion.technique_page_id == page.id,
            TechniquePageVersion.version_number == version_number,
        )
    )
    version_result = await db.execute(version_stmt)
    version = version_result.scalar_one_or_none()
    if version is None:
        raise HTTPException(
            status_code=404,
            detail=f"Version {version_number} not found for technique '{slug}'",
        )

    return TechniquePageVersionDetail.model_validate(version)
