"""Автодетализация задач: маленькая LLM (Ollama) предлагает черновые метаданные.

Предложение — НЕ окончательная детализация (см. docs/TZ.md 3.2/3.10): оно
применяется к задаче только при утверждении пользователем («Да, всё верно»
или после ручной правки). Ожидаемы промахи — это нормально.
"""

import json
import logging
from typing import Any

import httpx
from sqlalchemy import select

from app.config import get_settings
from app.db import get_session_factory
from app.models import Project, Tag, Task

logger = logging.getLogger(__name__)

MAX_TAGS = 3
PRIORITY_SCALE = "10 — срочно и важно; 7 — важно; 4 — обычное; 1 — когда-нибудь; null — неясно"


def build_prompt(
    title: str, description: str, tag_names: list[str], project_names: list[str]
) -> str:
    return (
        "Ты — ассистент личного таск-менеджера. Подбери черновые метаданные для новой задачи.\n\n"
        f"Доступные теги (выбирай ТОЛЬКО из них, максимум {MAX_TAGS}): "
        f"{json.dumps(tag_names, ensure_ascii=False) or '[]'}\n"
        "Доступные проекты: "
        f"{json.dumps(project_names, ensure_ascii=False) or '[]'} — "
        "или предложи краткое название нового проекта, если ни один не подходит.\n\n"
        f'Задача: "{title}"\n'
        f'Описание: "{description}"\n\n'
        f"Шкала приоритета: {PRIORITY_SCALE}.\n\n"
        'Ответь ТОЛЬКО JSON вида: {"tags": ["тег", ...], "project": "имя или null", '
        '"new_project": true или false, "priority": число или null}'
    )


class DetailingService:
    """Вызов LLM и разбор ответа. generate() изолирован для тестов."""

    def __init__(self, base_url: str | None = None, model: str | None = None) -> None:
        settings = get_settings()
        self.base_url = (base_url or settings.ollama_base_url).rstrip("/")
        self.model = model or settings.ollama_model

    def generate(self, prompt: str) -> str:
        response = httpx.post(
            f"{self.base_url}/api/generate",
            json={"model": self.model, "prompt": prompt, "format": "json", "stream": False},
            timeout=180.0,
        )
        response.raise_for_status()
        return str(response.json()["response"])

    def propose(
        self, title: str, description: str, tag_names: list[str], project_names: list[str]
    ) -> dict[str, Any] | None:
        prompt = build_prompt(title, description, tag_names, project_names)
        try:
            raw = self.generate(prompt)
            data = json.loads(raw)
        except Exception:
            logger.warning("Detailing LLM call failed for task %r", title, exc_info=True)
            return None

        if not isinstance(data, dict):
            return None

        known_tags = set(t.lower() for t in tag_names)
        tags = [t for t in data.get("tags") or [] if isinstance(t, str) and t.lower() in known_tags]
        tags = tags[:MAX_TAGS]

        priority = data.get("priority")
        if priority is not None and not (isinstance(priority, int) and 0 <= priority <= 10):
            priority = None

        project = data.get("project")
        if not isinstance(project, str) or not project.strip():
            project = None

        return {
            "tags": tags,
            "project": project,
            "new_project": bool(data.get("new_project")) or project not in project_names,
            "priority": priority,
        }


def apply_proposal(db: Any, task: Task, proposal: dict[str, Any]) -> None:
    """Применить предложение к задаче: теги (только существующие), проект, приоритет."""
    tag_names = proposal.get("tags") or []
    if tag_names:
        tags = [
            t
            for t in db.scalars(select(Tag)).all()
            if t.name.lower() in {n.lower() for n in tag_names}
        ]
        task.tags = list(tags)

    project_name = proposal.get("project")
    if project_name:
        project = db.scalars(
            select(Project).where(Project.name.ilike(project_name))
        ).first()
        if project is None and proposal.get("new_project"):
            project = Project(name=project_name.strip())
            db.add(project)
            db.flush()
        if project is not None:
            task.project_id = project.id

    if proposal.get("priority") is not None:
        task.priority = proposal["priority"]


def detail_task(task_id: int) -> None:
    """Фоновая работа: сгенерировать и сохранить предложение для задачи в стеке."""
    session = get_session_factory()()
    try:
        task = session.get(Task, task_id)
        if task is None or task.detail_state != "raw":
            return

        service = DetailingService()
        proposal = service.propose(
            task.title,
            task.description,
            [t.name for t in session.scalars(select(Tag)).all()],
            [p.name for p in session.scalars(select(Project)).all()],
        )
        if proposal is not None:
            task.ai_proposal = proposal
            session.commit()
    finally:
        session.close()
