from __future__ import annotations

import json

from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy import func, select

from services.shared.core import Role, new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.kb_localization import normalize_kb_language, resolve_article_group_id
from services.shared.kb_search import search_kb_rows
from services.shared.models import (
    HealthResponse,
    KBArticleCreate,
    KBArticleOut,
    KBArticleUpdate,
    KBCategoryCreate,
    KBCategoryOut,
)
from services.shared.security import require_roles
from services.shared.sql_init import init_sql_schema
from services.shared.sql_models import KBArticleRow, KBCategoryRow

app = FastAPI(title="kb-service", version="1.0.0")

init_sql_schema()


def _category_out(row: KBCategoryRow) -> KBCategoryOut:
    return KBCategoryOut(
        category_id=row.category_id,
        name=row.name,
        description=row.description,
        created_at=row.created_at,
    )


def _article_out(row: KBArticleRow) -> KBArticleOut:
    return KBArticleOut(
        article_id=row.article_id,
        category_id=row.category_id,
        article_group_id=resolve_article_group_id(row.article_id, row.article_group_id),
        language=normalize_kb_language(row.language),
        title=row.title,
        body=row.body,
        tags=json.loads(row.tags_json or "[]"),
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


def _article_group_expr():
    return func.coalesce(KBArticleRow.article_group_id, KBArticleRow.article_id)


def _find_group_language_conflict(
    session,
    *,
    article_group_id: str,
    language: str,
    exclude_article_id: str | None = None,
) -> KBArticleRow | None:
    stmt = select(KBArticleRow).where(_article_group_expr() == article_group_id).where(KBArticleRow.language == language)
    if exclude_article_id:
        stmt = stmt.where(KBArticleRow.article_id != exclude_article_id)
    return session.execute(stmt).scalar_one_or_none()


@app.get("/health", response_model=HealthResponse)
def health() -> HealthResponse:
    return HealthResponse(status="ok", service="kb-service")


@app.post("/knowledge/categories", response_model=KBCategoryOut)
def create_category(
    payload: KBCategoryCreate,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.ANALYST)),
) -> KBCategoryOut:
    session = get_session()
    try:
        row = KBCategoryRow(
            category_id=new_id("kbc"),
            name=payload.name,
            description=payload.description,
            created_at=utc_now_iso(),
        )
        session.add(row)
        session.commit()
        session.refresh(row)
        return _category_out(row)
    finally:
        session.close()


@app.get("/knowledge/categories", response_model=list[KBCategoryOut])
def list_categories() -> list[KBCategoryOut]:
    session = get_session()
    try:
        rows = session.execute(select(KBCategoryRow).order_by(KBCategoryRow.id.asc())).scalars().all()
        return [_category_out(r) for r in rows]
    finally:
        session.close()


@app.post("/knowledge/articles", response_model=KBArticleOut)
def create_article(
    payload: KBArticleCreate,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.ANALYST)),
) -> KBArticleOut:
    session = get_session()
    try:
        category = session.execute(
            select(KBCategoryRow).where(KBCategoryRow.category_id == payload.category_id)
        ).scalar_one_or_none()
        if not category:
            raise HTTPException(status_code=400, detail="Unknown category_id")

        now = utc_now_iso()
        article_id = new_id("kba")
        language = normalize_kb_language(payload.language)
        article_group_id = resolve_article_group_id(article_id, payload.article_group_id)
        conflict = _find_group_language_conflict(
            session,
            article_group_id=article_group_id,
            language=language,
        )
        if conflict:
            raise HTTPException(
                status_code=409,
                detail="Localized article already exists for this article_group_id and language",
            )
        row = KBArticleRow(
            article_id=article_id,
            category_id=payload.category_id,
            article_group_id=article_group_id,
            language=language,
            title=payload.title,
            body=payload.body,
            tags_json=json.dumps(payload.tags, ensure_ascii=False),
            created_at=now,
            updated_at=now,
        )
        session.add(row)
        session.commit()
        session.refresh(row)
        return _article_out(row)
    finally:
        session.close()


@app.get("/knowledge/articles/{article_id}", response_model=KBArticleOut)
def get_article(article_id: str) -> KBArticleOut:
    session = get_session()
    try:
        row = session.execute(
            select(KBArticleRow).where(KBArticleRow.article_id == article_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Article not found")
        return _article_out(row)
    finally:
        session.close()


@app.patch("/knowledge/articles/{article_id}", response_model=KBArticleOut)
def update_article(
    article_id: str,
    payload: KBArticleUpdate,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.ANALYST)),
) -> KBArticleOut:
    session = get_session()
    try:
        row = session.execute(
            select(KBArticleRow).where(KBArticleRow.article_id == article_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Article not found")

        data = payload.model_dump(exclude_none=True)
        target_group_id = resolve_article_group_id(
            row.article_id,
            data.get("article_group_id", row.article_group_id),
        )
        target_language = normalize_kb_language(data.get("language", row.language))
        conflict = _find_group_language_conflict(
            session,
            article_group_id=target_group_id,
            language=target_language,
            exclude_article_id=row.article_id,
        )
        if conflict:
            raise HTTPException(
                status_code=409,
                detail="Localized article already exists for this article_group_id and language",
            )

        if "article_group_id" in data:
            row.article_group_id = target_group_id
        if "language" in data:
            row.language = target_language
        if "title" in data:
            row.title = data["title"]
        if "body" in data:
            row.body = data["body"]
        if "tags" in data:
            row.tags_json = json.dumps(data["tags"], ensure_ascii=False)
        row.updated_at = utc_now_iso()
        session.commit()
        return _article_out(row)
    finally:
        session.close()


@app.get("/knowledge/search", response_model=list[KBArticleOut])
def search_articles(
    q: str = "",
    limit: int = 50,
    language: str | None = None,
    article_group_id: str | None = None,
) -> list[KBArticleOut]:
    session = get_session()
    try:
        stmt = select(KBArticleRow).order_by(KBArticleRow.id.desc())
        if language is not None:
            stmt = stmt.where(KBArticleRow.language == normalize_kb_language(language))
        if article_group_id:
            stmt = stmt.where(_article_group_expr() == article_group_id.strip())
        rows = session.execute(stmt).scalars().all()
        matches = search_kb_rows(rows, q, limit=limit, empty_query_returns_all=True)
        return [_article_out(row) for row in matches]
    finally:
        session.close()
