"""Pre-processing helpers for loom_decompose — keeps tools.py thin."""

from __future__ import annotations

from pathlib import Path

import asyncpg
from redis.asyncio import Redis

from loom.config import SkillsConfig
from loom.graph import cache, store
from loom.graph.task import TaskStatus


async def resolve_epic_context(
    pool: asyncpg.Pool, redis: Redis, project_id: str,
    epic_id: str, goal: str, context: str | None, force_leaf: bool,
) -> dict:
    """Resolve epic into decompose parameters.

    Returns dict with keys: goal, context, parent_epic_id, force_leaf, epic.
    Raises ValueError on invalid epic.
    """
    epic = await cache.get_task(redis, pool, project_id, epic_id)
    if epic.status == TaskStatus.IDEA:
        epic = await store.promote_idea(pool, epic_id)
        await cache.sync_task(redis, epic)
    elif epic.status != TaskStatus.EPIC:
        raise ValueError(f"Task '{epic_id}' is '{epic.status}', not an epic.")

    resolved_goal = goal or f"Decompose: {epic.title}"
    epic_ctx = epic.context.get("description", "") if isinstance(epic.context, dict) else ""
    resolved_context = context if context else (epic_ctx or None)
    resolved_force_leaf = True if not force_leaf else force_leaf

    # Existing-task injection now handled by inject_existing_work_context
    # for all decompositions (not just epic).

    return {
        "goal": resolved_goal,
        "context": resolved_context,
        "parent_epic_id": epic_id,
        "force_leaf": resolved_force_leaf,
        "epic": epic,
    }


def inject_granularity(context: str | None, granularity: str | None) -> str | None:
    """Append granularity guidance to context string."""
    if not granularity:
        return context
    gran_map = {
        "coarse": "\n\nGRANULARITY: Create fewer, larger tasks (3-5 per epic). Each task may span multiple files and take a full work session. Suitable for a single agent working sequentially.",
        "fine": "\n\nGRANULARITY: Create many small, focused tasks (5-10 per epic). Each task should be one unit of work for a single subagent. Suitable for multi-agent parallel dispatch.",
    }
    if granularity in gran_map:
        return (context or "") + gran_map[granularity]
    return context


async def inject_dependency_hints(
    pool: asyncpg.Pool, redis: Redis, project_id: str,
    context: str | None, dependency_hints: dict,
) -> str | None:
    """Resolve dependency hint task IDs and append to context."""
    hint_parts = []
    if dependency_hints.get("after"):
        after_tasks = []
        for tid in dependency_hints["after"]:
            try:
                t = await cache.get_task(redis, pool, project_id, tid)
                after_tasks.append(f"- {t.title} ({t.id})")
            except Exception:
                after_tasks.append(f"- {tid}")
        hint_parts.append(
            "ALL tasks in this decomposition MUST wait for these to complete first:\n"
            + "\n".join(after_tasks)
        )
    if dependency_hints.get("before"):
        before_tasks = []
        for tid in dependency_hints["before"]:
            try:
                t = await cache.get_task(redis, pool, project_id, tid)
                before_tasks.append(f"- {t.title} ({t.id})")
            except Exception:
                before_tasks.append(f"- {tid}")
        hint_parts.append(
            "These tasks are waiting on this decomposition to complete:\n"
            + "\n".join(before_tasks)
        )
    if hint_parts:
        return (context or "") + "\n\nDEPENDENCY CONTEXT:\n" + "\n".join(hint_parts)
    return context


async def resolve_parent_epic_deps(
    pool: asyncpg.Pool, redis: Redis, project_id: str,
    epic_id: str | None, dependency_hints: dict | None,
) -> list[str] | None:
    """Collect parent epic deps + 'after' hints for child inheritance."""
    parent_epic_deps: list[str] | None = None
    if epic_id:
        epic_obj = await cache.get_task(redis, pool, project_id, epic_id)
        if epic_obj.depends_on:
            parent_epic_deps = list(epic_obj.depends_on)
    if dependency_hints and dependency_hints.get("after"):
        if parent_epic_deps is None:
            parent_epic_deps = []
        for tid in dependency_hints["after"]:
            if tid not in parent_epic_deps:
                parent_epic_deps.append(tid)
    return parent_epic_deps


async def inject_existing_work_context(
    pool: asyncpg.Pool, project_id: str,
    context: str | None, exclude_ids: set[str] | None = None,
) -> str | None:
    """Inject existing project tasks into the decompose context.

    Fetches in-progress AND completed tasks so the LLM knows both what's
    underway and what's already been built. This prevents duplicate task
    generation and lets the decomposer focus on genuinely new work.
    """
    exclude = exclude_ids or set()

    in_progress = []
    for status in ("pending", "blocked", "claimed", "epic"):
        in_progress += await store.get_tasks_by_status(pool, project_id, status, limit=200)

    done = await store.get_tasks_by_status(pool, project_id, "done", limit=200)

    lines: list[str] = []
    for t in in_progress:
        if t.id not in exclude:
            lines.append(f"- [{t.status.upper()}] {t.title}")
    for t in done:
        if t.id not in exclude:
            lines.append(f"- [DONE] {t.title}")

    if not lines:
        return context

    section = (
        "\n\nEXISTING WORK IN THIS PROJECT (do NOT create duplicates):\n"
        "Tasks marked [DONE] are already completed — do not regenerate them.\n"
        "Tasks marked with other statuses are in progress — do not duplicate them.\n"
        + "\n".join(lines)
    )
    return (context or "") + section


async def validate_decompose(
    pool: asyncpg.Pool, project_id: str, api_key: str, provider: str,
) -> dict | None:
    """Pre-flight checks for decompose. Returns error dict or None."""
    if not project_id:
        return {"error": "No active project. Create one with loom_create_project first."}
    if not api_key:
        env_hint = "OPENAI_API_KEY" if provider == "openai" else "ANTHROPIC_API_KEY"
        return {"error": f"No API key configured. Set {env_hint} or LOOM_SKILLS_API_KEY env var, or add skills.api_key to ~/.loom/config.yaml."}
    try:
        from loom.graph.project import get_project
        await get_project(pool, project_id)
    except Exception:
        return {"error": f"Project '{project_id}' not found. Create one with loom_create_project and loom_switch_project first."}
    return None


async def run_decompose(
    goal: str, context: str | None, project_id: str,
    skills_config: SkillsConfig, pool: asyncpg.Pool, redis: Redis,
    local_dir: str | None, confirm: bool, depth: int, enrich: bool,
    optimize_parallelism: bool, parent_epic_id: str | None,
    force_leaf: bool, parent_epic_deps: list[str] | None,
    auto_fix_paths: bool, merge_file_affinity: bool,
    proposal_ttl_seconds: int = 1800,
) -> dict:
    """Run the decomposer and return the result as a dict."""
    from loom.skills.decomposer import decompose
    result = await decompose(
        goal=goal, project_id=project_id, config=skills_config,
        pool=pool, redis=redis, context=context, confirm=confirm,
        project_dir=local_dir, depth=depth, enrich=enrich,
        scan_root=Path(local_dir) if local_dir else None,
        optimize_parallelism=optimize_parallelism,
        parent_epic_id=parent_epic_id,
        force_leaf=force_leaf,
        parent_epic_deps=parent_epic_deps,
        auto_fix_paths=auto_fix_paths,
        merge_file_affinity=merge_file_affinity,
        proposal_ttl_seconds=proposal_ttl_seconds,
    )
    return result.model_dump(mode="json")


async def prepare_and_run_decompose(
    pool: asyncpg.Pool, redis: Redis, project_id: str,
    skills_config: SkillsConfig, local_dir: str | None,
    goal: str, context: str | None, confirm: bool, depth: int, enrich: bool,
    optimize_parallelism: bool, parent_epic_id: str | None,
    epic_id: str | None, force_leaf: bool, granularity: str | None,
    dependency_hints: dict | None, auto_fix_paths: bool, merge_file_affinity: bool,
    proposal_ttl_seconds: int = 1800,
) -> dict:
    """Full decompose pipeline: validate, resolve epic, prep context, run."""
    err = await validate_decompose(pool, project_id, skills_config.api_key, skills_config.provider)
    if err:
        return err
    if epic_id:
        try:
            resolved = await resolve_epic_context(pool, redis, project_id, epic_id, goal, context, force_leaf)
        except ValueError as e:
            return {"error": str(e)}
        goal, context = resolved["goal"], resolved["context"]
        parent_epic_id, force_leaf = resolved["parent_epic_id"], resolved["force_leaf"]
    if not goal:
        return {"error": "Provide a goal or epic_id to decompose."}
    # Inject existing work (in-progress + done) for all decompositions
    exclude_ids = {epic_id} if epic_id else None
    context = await inject_existing_work_context(pool, project_id, context, exclude_ids)
    context = inject_granularity(context, granularity)
    if dependency_hints:
        context = await inject_dependency_hints(pool, redis, project_id, context, dependency_hints)
    parent_epic_deps = await resolve_parent_epic_deps(pool, redis, project_id, epic_id, dependency_hints)
    return await run_decompose(
        goal, context, project_id, skills_config, pool, redis, local_dir,
        confirm, depth, enrich, optimize_parallelism, parent_epic_id,
        force_leaf, parent_epic_deps, auto_fix_paths, merge_file_affinity,
        proposal_ttl_seconds,
    )
