import logging
import json
import re
from typing import Any, Dict, List, Literal, Optional

from fastapi import BackgroundTasks, APIRouter, HTTPException, Query
from pydantic import BaseModel, Field

from app.modules.manual.backend.cache import manual_cache
from app.core.database import execute_query, execute_query_single, execute_update

logger = logging.getLogger(__name__)
router = APIRouter()

DifficultyType = Literal["beginner", "advanced"]


class ManualStepInput(BaseModel):
    step_number: int = Field(ge=1)
    title: str = Field(min_length=1, max_length=255)
    content: str = Field(min_length=1)
    image_url: Optional[str] = None
    video_url: Optional[str] = None


class ManualRelationInput(BaseModel):
    related_module: Optional[str] = Field(default=None, max_length=80)
    related_tag: Optional[str] = Field(default=None, max_length=120)
    related_sag_type: Optional[str] = Field(default=None, max_length=120)
    related_manual_id: Optional[str] = None


class ManualArticleCreate(BaseModel):
    title: str = Field(min_length=1, max_length=255)
    slug: Optional[str] = Field(default=None, max_length=255)
    content: str = Field(min_length=1)
    summary: Optional[str] = None
    module: str = Field(min_length=1, max_length=80)
    tags: List[str] = Field(default_factory=list)
    difficulty: DifficultyType = "beginner"
    steps: List[ManualStepInput] = Field(default_factory=list)
    relations: List[ManualRelationInput] = Field(default_factory=list)


class ManualArticleUpdate(BaseModel):
    title: Optional[str] = Field(default=None, min_length=1, max_length=255)
    slug: Optional[str] = Field(default=None, max_length=255)
    content: Optional[str] = Field(default=None, min_length=1)
    summary: Optional[str] = None
    module: Optional[str] = Field(default=None, min_length=1, max_length=80)
    tags: Optional[List[str]] = None
    difficulty: Optional[DifficultyType] = None
    steps: Optional[List[ManualStepInput]] = None
    relations: Optional[List[ManualRelationInput]] = None


def _slugify(value: str) -> str:
    cleaned = re.sub(r"[^a-zA-Z0-9\s-]", "", value or "").strip().lower()
    cleaned = re.sub(r"[-\s]+", "-", cleaned)
    return cleaned[:255] or "manual"


def _normalize_tags(tags: Optional[List[str]]) -> List[str]:
    if not tags:
        return []
    out: List[str] = []
    seen = set()
    for tag in tags:
        clean = str(tag or "").strip().lower()
        if not clean:
            continue
        if clean in seen:
            continue
        seen.add(clean)
        out.append(clean)
    return out


def _next_unique_slug(base_slug: str, exclude_id: Optional[str] = None) -> str:
    candidate = _slugify(base_slug)
    suffix = 1
    while True:
        if exclude_id:
            row = execute_query_single(
                "SELECT id FROM manual_articles WHERE slug = %s AND id <> %s AND deleted_at IS NULL",
                (candidate, exclude_id),
            )
        else:
            row = execute_query_single(
                "SELECT id FROM manual_articles WHERE slug = %s AND deleted_at IS NULL",
                (candidate,),
            )
        if not row:
            return candidate
        candidate = f"{_slugify(base_slug)}-{suffix}"
        suffix += 1


def _fetch_article_by_id(article_id: str) -> Optional[Dict[str, Any]]:
    row = execute_query_single(
        """
        SELECT id, title, slug, content, summary, module, tags, difficulty,
               use_count, created_at, updated_at
        FROM manual_articles
        WHERE id = %s AND deleted_at IS NULL
        """,
        (article_id,),
    )
    if not row:
        return None
    return _expand_article(row)


def _expand_article(row: Dict[str, Any]) -> Dict[str, Any]:
    article = dict(row)
    steps = execute_query(
        """
        SELECT id, step_number, title, content, image_url, video_url
        FROM manual_steps
        WHERE manual_id = %s
        ORDER BY step_number ASC
        """,
        (article["id"],),
    ) or []

    relations = execute_query(
        """
        SELECT
            mr.id,
            mr.related_module,
            mr.related_tag,
            mr.related_sag_type,
            mr.related_manual_id,
            rm.slug AS related_manual_slug,
            rm.title AS related_manual_title
        FROM manual_relations mr
        LEFT JOIN manual_articles rm
            ON rm.id = mr.related_manual_id
           AND rm.deleted_at IS NULL
        WHERE mr.manual_id = %s
        ORDER BY rm.use_count DESC NULLS LAST, rm.updated_at DESC NULLS LAST
        """,
        (article["id"],),
    ) or []

    article["steps"] = steps
    article["relations"] = relations
    return article


def _replace_steps(article_id: str, steps: List[ManualStepInput]) -> None:
    execute_update("DELETE FROM manual_steps WHERE manual_id = %s", (article_id,))
    for step in sorted(steps, key=lambda s: s.step_number):
        execute_query(
            """
            INSERT INTO manual_steps (manual_id, step_number, title, content, image_url, video_url)
            VALUES (%s, %s, %s, %s, %s, %s)
            """,
            (
                article_id,
                step.step_number,
                step.title,
                step.content,
                step.image_url,
                step.video_url,
            ),
        )


def _replace_relations(article_id: str, relations: List[ManualRelationInput]) -> None:
    execute_update("DELETE FROM manual_relations WHERE manual_id = %s", (article_id,))
    for relation in relations:
        execute_query(
            """
            INSERT INTO manual_relations (manual_id, related_module, related_tag, related_sag_type, related_manual_id)
            VALUES (%s, %s, %s, %s, %s)
            """,
            (
                article_id,
                relation.related_module,
                relation.related_tag,
                relation.related_sag_type,
                relation.related_manual_id,
            ),
        )


@router.get("/manual")
async def list_manual_articles(
    module: Optional[str] = Query(default=None),
    difficulty: Optional[DifficultyType] = Query(default=None),
    tags: Optional[str] = Query(default=None, description="Comma-separated tags"),
    search: Optional[str] = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
):
    where_parts = ["deleted_at IS NULL"]
    params: List[Any] = []

    if module:
        where_parts.append("LOWER(module) = LOWER(%s)")
        params.append(module.strip())

    if difficulty:
        where_parts.append("difficulty = %s")
        params.append(difficulty)

    if tags:
        tag_list = _normalize_tags([t.strip() for t in tags.split(",") if t.strip()])
        if tag_list:
            where_parts.append(
                "EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(tags, '[]'::jsonb)) t(tag) WHERE t.tag = ANY(%s::text[]))"
            )
            params.append(tag_list)

    if search:
        where_parts.append("(title ILIKE %s OR summary ILIKE %s OR content ILIKE %s)")
        needle = f"%{search.strip()}%"
        params.extend([needle, needle, needle])

    query = f"""
        SELECT id, title, slug, summary, module, tags, difficulty, use_count, created_at, updated_at
        FROM manual_articles
        WHERE {' AND '.join(where_parts)}
        ORDER BY use_count DESC, updated_at DESC, created_at DESC
        LIMIT %s OFFSET %s
    """
    params.extend([limit, offset])

    items = execute_query(query, tuple(params)) or []
    return {"items": items, "count": len(items)}


@router.get("/manual/context")
async def contextual_manual_suggestions(
    module: Optional[str] = Query(default=None),
    tag: Optional[str] = Query(default=None),
    sag_type: Optional[str] = Query(default=None),
    limit: int = Query(default=10, ge=1, le=50),
):
    where_parts = ["ma.deleted_at IS NULL"]
    params: List[Any] = []

    if module:
        where_parts.append("(LOWER(ma.module) = LOWER(%s) OR LOWER(mr.related_module) = LOWER(%s))")
        params.extend([module.strip(), module.strip()])

    if tag:
        where_parts.append(
            "(LOWER(mr.related_tag) = LOWER(%s) OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(ma.tags, '[]'::jsonb)) t(tag) WHERE LOWER(t.tag) = LOWER(%s)))"
        )
        params.extend([tag.strip(), tag.strip()])

    if sag_type:
        where_parts.append("LOWER(mr.related_sag_type) = LOWER(%s)")
        params.append(sag_type.strip())

    query = f"""
        SELECT DISTINCT
            ma.id,
            ma.title,
            ma.slug,
            ma.summary,
            ma.module,
            ma.tags,
            ma.difficulty,
            ma.use_count,
            ma.updated_at
        FROM manual_articles ma
        LEFT JOIN manual_relations mr ON mr.manual_id = ma.id
        WHERE {' AND '.join(where_parts)}
        ORDER BY ma.use_count DESC, ma.updated_at DESC
        LIMIT %s
    """
    params.append(limit)

    items = execute_query(query, tuple(params)) or []
    return {"items": items, "count": len(items)}


@router.get("/manual/{slug}")
async def get_manual_article(slug: str, background_tasks: BackgroundTasks):
    cache_key = f"slug:{slug}"
    cached = manual_cache.get(cache_key)
    
    if cached:
        background_tasks.add_task(_increment_use_count, cached["id"])
        return cached
    article = execute_query_single(
        """
        SELECT id, title, slug, content, summary, module, tags, difficulty, use_count, created_at, updated_at
        FROM manual_articles
        WHERE slug = %s AND deleted_at IS NULL
        """,
        (slug,),
    )
    if not article:
        raise HTTPException(status_code=404, detail="Manual not found")

    execute_update(
        "UPDATE manual_articles SET use_count = COALESCE(use_count, 0) + 1 WHERE id = %s",
        (article["id"],),
    )

    # Read the refreshed count for consistency in response.
    article["use_count"] = int(article.get("use_count") or 0) + 1
    return _expand_article(article)


@router.post("/manual")
async def create_manual_article(payload: ManualArticleCreate):
    try:
        wanted_slug = payload.slug or payload.title
        slug = _next_unique_slug(wanted_slug)

        created = execute_query_single(
            """
            INSERT INTO manual_articles (title, slug, content, summary, module, tags, difficulty)
            VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s)
            RETURNING id
            """,
            (
                payload.title.strip(),
                slug,
                payload.content,
                payload.summary,
                payload.module.strip().lower(),
                json.dumps(_normalize_tags(payload.tags), ensure_ascii=False),
                payload.difficulty,
            ),
        )
        if not created:
            raise HTTPException(status_code=500, detail="Could not create manual")

        article_id = created["id"]
        if payload.steps:
            _replace_steps(article_id, payload.steps)
        if payload.relations:
            _replace_relations(article_id, payload.relations)

        article = _fetch_article_by_id(article_id)
        if not article:
            raise HTTPException(status_code=500, detail="Could not load created manual")
        return article
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Failed to create manual article: %s", e, exc_info=True)
        raise HTTPException(status_code=500, detail="Failed to create manual")


@router.put("/manual/{article_id}")
async def update_manual_article(article_id: str, payload: ManualArticleUpdate):
    existing = execute_query_single(
        "SELECT id, title, slug FROM manual_articles WHERE id = %s AND deleted_at IS NULL",
        (article_id,),
    )
    if not existing:
        raise HTTPException(status_code=404, detail="Manual not found")

    updates: List[str] = []
    params: List[Any] = []

    if payload.title is not None:
        updates.append("title = %s")
        params.append(payload.title.strip())

    if payload.content is not None:
        updates.append("content = %s")
        params.append(payload.content)

    if payload.summary is not None:
        updates.append("summary = %s")
        params.append(payload.summary)

    if payload.module is not None:
        updates.append("module = %s")
        params.append(payload.module.strip().lower())

    if payload.difficulty is not None:
        updates.append("difficulty = %s")
        params.append(payload.difficulty)

    if payload.tags is not None:
        updates.append("tags = %s::jsonb")
        params.append(json.dumps(_normalize_tags(payload.tags), ensure_ascii=False))

    if payload.slug is not None or payload.title is not None:
        slug_source = payload.slug or payload.title or existing["slug"]
        unique_slug = _next_unique_slug(slug_source, exclude_id=article_id)
        updates.append("slug = %s")
        params.append(unique_slug)

    if updates:
        params.append(article_id)
        execute_update(
            f"UPDATE manual_articles SET {', '.join(updates)} WHERE id = %s",
            tuple(params),
        )

    if payload.steps is not None:
        _replace_steps(article_id, payload.steps)

    if payload.relations is not None:
        _replace_relations(article_id, payload.relations)

    article = _fetch_article_by_id(article_id)
    if not article:
        raise HTTPException(status_code=500, detail="Could not load updated manual")
    return article


@router.delete("/manual/{article_id}")
async def delete_manual_article(article_id: str):
    affected = execute_update(
        "UPDATE manual_articles SET deleted_at = CURRENT_TIMESTAMP WHERE id = %s AND deleted_at IS NULL",
        (article_id,),
    )
    if affected == 0:
        raise HTTPException(status_code=404, detail="Manual not found")
    return {"success": True, "deleted_id": article_id}
