"""Background task-body consolidation pipeline.

Reads a task's description (user goal) + work logs (chronological) and writes
a 1-3 paragraph summary into Note.body via the background model. Triggered by
log accumulation (debounced), status transitions to terminal states, and a
manual API endpoint.

Design: docs/superpowers/specs/2026-05-13-task-as-durable-record-design.md
"""
from __future__ import annotations

import asyncio
import logging
from collections import defaultdict
from datetime import datetime, timezone

from sqlalchemy import func, select

from fabledassistant.models import async_session
from fabledassistant.models.note import Note
from fabledassistant.models.task_log import TaskLog

logger = logging.getLogger(__name__)

# Trigger thresholds. Tunable as constants; could be promoted to env vars if
# the defaults prove wrong in practice.
DEFAULT_LOG_THRESHOLD = 3
MAX_LOGS_FOR_PROMPT = 50
MAX_PROMPT_INPUT_CHARS = 8000

# Per-task asyncio locks to prevent two simultaneous consolidations of the
# same task. Single-process; no cross-process coordination needed.
_locks: dict[int, asyncio.Lock] = defaultdict(asyncio.Lock)


async def _logs_since_last_consolidation(user_id: int, task_id: int) -> int:
    """Count work logs that arrived after the most recent consolidation pass.

    Returns 0 if the task doesn't exist. Returns the total log count when
    consolidated_at is NULL (i.e. never consolidated).
    """
    async with async_session() as session:
        task = (
            await session.execute(
                select(Note).where(Note.id == task_id, Note.user_id == user_id)
            )
        ).scalars().first()
        if task is None:
            return 0
        stmt = select(func.count(TaskLog.id)).where(
            TaskLog.task_id == task_id, TaskLog.user_id == user_id,
        )
        if task.consolidated_at is not None:
            stmt = stmt.where(TaskLog.created_at > task.consolidated_at)
        return int((await session.execute(stmt)).scalar() or 0)


async def _auto_consolidate_enabled(user_id: int) -> bool:
    """User-level setting; default true. Manual endpoint bypasses this."""
    from fabledassistant.services.settings import get_setting
    val = await get_setting(user_id, "auto_consolidate_tasks", "true")
    return str(val).lower() in ("true", "1", "yes")


async def maybe_consolidate(user_id: int, task_id: int, *, reason: str) -> None:
    """Debounced gate. Decides whether to schedule a consolidate_task pass.

    reason='log_added'   — gated by log count >= DEFAULT_LOG_THRESHOLD
    reason='task_closed' — proceeds unconditionally (subject to setting)
    """
    if not await _auto_consolidate_enabled(user_id):
        return
    if reason == "log_added":
        n = await _logs_since_last_consolidation(user_id, task_id)
        if n < DEFAULT_LOG_THRESHOLD:
            return
    elif reason != "task_closed":
        logger.warning("maybe_consolidate: unknown reason %r", reason)
        return
    # Fire-and-forget; consolidate_task handles its own errors.
    asyncio.create_task(consolidate_task(user_id, task_id))


def _build_consolidation_prompt(
    *, title: str, description: str | None, logs: list,
) -> str:
    """Build the LLM prompt for one consolidation pass.

    Caps total log content at MAX_PROMPT_INPUT_CHARS; logs that don't fit
    are dropped. Caller is expected to slice to the most-recent window
    before calling.
    """
    log_lines: list[str] = []
    chars = 0
    for log in logs:
        ts = log.created_at.isoformat() if getattr(log, "created_at", None) else "?"
        line = f"- [{ts}] {log.content}"
        if chars + len(line) > MAX_PROMPT_INPUT_CHARS:
            break
        log_lines.append(line)
        chars += len(line)

    return (
        "You are summarizing the work done on a task. The user wrote the goal "
        "below; do not restate it. Read the chronological work-log entries and "
        "produce a 1-3 paragraph summary of: what was attempted, what worked, "
        "what failed, what's current state. Use the user's voice; cite specific "
        "commands/decisions; favor brevity over completeness. Output plain "
        "markdown body content only — no preamble.\n\n"
        f"TITLE: {title}\n"
        f"GOAL (read-only context): {description or '(no goal recorded)'}\n"
        f"WORK LOG (chronological):\n" + "\n".join(log_lines)
    )


async def consolidate_task(user_id: int, task_id: int) -> None:
    """Run a consolidation pass: read description + logs, write summary to body.

    Errors are logged and swallowed so the fire-and-forget caller is never
    interrupted; on LLM failure the body and consolidated_at are left
    untouched and the next trigger retries.
    """
    lock = _locks[task_id]
    if lock.locked():
        logger.debug(
            "consolidate_task: skipping — already running for task %d", task_id
        )
        return
    async with lock:
        try:
            async with async_session() as session:
                task = (
                    await session.execute(
                        select(Note).where(
                            Note.id == task_id, Note.user_id == user_id,
                        )
                    )
                ).scalars().first()
                if task is None or not task.status:
                    return  # not a task, or missing
                logs = (
                    await session.execute(
                        select(TaskLog)
                        .where(
                            TaskLog.task_id == task_id,
                            TaskLog.user_id == user_id,
                        )
                        .order_by(TaskLog.created_at.asc())
                    )
                ).scalars().all()

                if not logs:
                    return  # nothing to summarize yet

                title = task.title or ""
                description = task.description
                window = logs[-MAX_LOGS_FOR_PROMPT:]

            prompt = _build_consolidation_prompt(
                title=title, description=description, logs=window,
            )

            from fabledassistant.services.llm import generate_completion
            from fabledassistant.services.settings import get_setting
            from fabledassistant.config import Config

            bg_model = await get_setting(
                user_id, "background_model", Config.OLLAMA_BACKGROUND_MODEL,
            )
            summary = await generate_completion(
                [{"role": "user", "content": prompt}],
                model=bg_model,
                max_tokens=800,
                num_ctx=4096,
            )
            if not summary or not summary.strip():
                return

            now = datetime.now(timezone.utc)
            async with async_session() as session:
                task = (
                    await session.execute(
                        select(Note).where(
                            Note.id == task_id, Note.user_id == user_id,
                        )
                    )
                ).scalars().first()
                if task is None:
                    return
                task.body = summary.strip()
                task.consolidated_at = now
                task.updated_at = now
                await session.commit()

            from fabledassistant.services.embeddings import upsert_note_embedding
            await upsert_note_embedding(
                task_id, user_id, f"{title}\n{summary.strip()}".strip(),
            )
            logger.info(
                "consolidate_task: refreshed task %d body (%d chars)",
                task_id, len(summary),
            )
        except Exception:
            logger.exception(
                "consolidate_task failed for task %d", task_id,
            )
