"""Task file management: numbering, querying, loading, and ordering.

Tasks are Markdown files with YAML frontmatter stored under an
initiative's ``tasks/`` directory::

    .autobuilder/initiatives/<initiative-id>/tasks/0001-my-task.md

Each task is a small, one-commit unit of work.
"""

import re
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple

from common import TASK_NUM_WIDTH
from frontmatter import parse_bool, parse_deps, read_frontmatter, strip_quotes

# ---------------------------------------------------------------------------
# Numbering
# ---------------------------------------------------------------------------


def max_task_number(tasks_dir: Path) -> int:
    """Return the highest numeric prefix found among ``*.md`` files."""
    highest = 0
    if tasks_dir.exists():
        for md in tasks_dir.glob("*.md"):
            m = re.match(r"^(\d+)-", md.name)
            if m:
                highest = max(highest, int(m.group(1)))
    return highest


def next_task_number(tasks_dir: Path) -> int:
    return max_task_number(tasks_dir) + 1


def numbered_filename(num: int, task_id: str) -> str:
    return f"{num:0{TASK_NUM_WIDTH}d}-{task_id}.md"


def normalize_task_filenames(tasks_dir: Path) -> int:
    """Rename un-numbered ``*.md`` files so they have a 4-digit prefix."""
    if not tasks_dir.exists():
        return 0
    count = 0
    for md in sorted(tasks_dir.glob("*.md")):
        if not re.match(r"^\d{4,}-", md.name):
            num = next_task_number(tasks_dir)
            new_name = numbered_filename(num, md.stem)
            md.rename(tasks_dir / new_name)
            count += 1
    return count


# ---------------------------------------------------------------------------
# Querying
# ---------------------------------------------------------------------------


def get_task_ids(tasks_dir: Path) -> Dict[str, Path]:
    """Map each task id → its file path."""
    result: Dict[str, Path] = {}
    if not tasks_dir.exists():
        return result
    for md in tasks_dir.glob("*.md"):
        fm = read_frontmatter(md)
        iid = fm.get("id") or md.stem
        result[iid] = md
    return result


def find_all_unplanned(tasks_dir: Path) -> List[Tuple[str, Path]]:
    result: List[Tuple[str, Path, int]] = []
    if not tasks_dir.exists():
        return []
    for md in sorted(tasks_dir.glob("*.md")):
        fm = read_frontmatter(md)
        if parse_bool(fm.get("split")):
            continue
        if parse_bool(fm.get("planned")) or parse_bool(fm.get("executed")):
            continue
        iid = fm.get("id") or md.stem
        raw_p = fm.get("priority", "0")
        try:
            priority = int(raw_p)
        except (ValueError, TypeError):
            priority = 0
        result.append((iid, md, priority))
    # Sort by priority: 1=highest first, 0/unset→999, then by id
    result.sort(key=lambda x: (x[2] if x[2] > 0 else 999, x[0]))
    return [(iid, md) for iid, md, _ in result]


def find_planned_not_executed(tasks_dir: Path) -> List[Tuple[str, Path]]:
    result: List[Tuple[str, Path, int]] = []
    if not tasks_dir.exists():
        return []
    for md in sorted(tasks_dir.glob("*.md")):
        fm = read_frontmatter(md)
        if parse_bool(fm.get("split")):
            continue
        if parse_bool(fm.get("planned")) and not parse_bool(fm.get("executed")):
            iid = fm.get("id") or md.stem
            raw_p = fm.get("priority", "0")
            try:
                priority = int(raw_p)
            except (ValueError, TypeError):
                priority = 0
            result.append((iid, md, priority))
    # Sort by priority: 1=highest first, 0/unset→999, then by id
    result.sort(key=lambda x: (x[2] if x[2] > 0 else 999, x[0]))
    return [(iid, md) for iid, md, _ in result]


def find_new_tasks(
    tasks_dir: Path, known_ids: Set[str],
) -> List[Tuple[str, Path]]:
    new: List[Tuple[str, Path]] = []
    if not tasks_dir.exists():
        return new
    for md in sorted(tasks_dir.glob("*.md")):
        fm = read_frontmatter(md)
        iid = fm.get("id") or md.stem
        if iid not in known_ids and not parse_bool(fm.get("executed")):
            new.append((iid, md))
    return new


# ---------------------------------------------------------------------------
# Area analysis (operates on a single tasks dir or across initiatives)
# ---------------------------------------------------------------------------


def extract_area(task_id: str, root_id: str = "") -> str:
    """Extract an area/component name from a task ID.

    Strips the *root_id* prefix (e.g. ``eview-mvp-``) and returns the
    first word, which usually corresponds to the component or page area.
    """
    name = task_id
    if root_id and name.startswith(root_id + "-"):
        name = name[len(root_id) + 1:]
    parts = name.split("-")
    if parts and parts[0].isdigit():
        parts = parts[1:]
    return parts[0].lower() if parts else "other"


def group_tasks_by_area(
    tasks_dir: Path, root_id: str = "",
) -> Dict[str, List[str]]:
    """Group executed tasks by area.  Returns ``{area: [titles]}``."""
    groups: Dict[str, List[str]] = {}
    if not tasks_dir.exists():
        return groups
    for md in sorted(tasks_dir.glob("*.md")):
        fm = read_frontmatter(md)
        if not parse_bool(fm.get("executed")):
            continue
        iid = fm.get("id", md.stem)
        title = strip_quotes(fm.get("title", iid))
        area = extract_area(iid, root_id)
        groups.setdefault(area, [])
        groups[area].append(title)
    return groups


def group_all_tasks_by_area(
    initiatives_base: Path, root_id: str = "",
) -> Dict[str, List[str]]:
    """Group executed tasks by area across ALL initiatives."""
    groups: Dict[str, List[str]] = {}
    if not initiatives_base.exists():
        return groups
    for init_dir in sorted(initiatives_base.iterdir()):
        td = init_dir / "tasks"
        if not td.is_dir():
            continue
        for area, titles in group_tasks_by_area(td, root_id).items():
            groups.setdefault(area, [])
            groups[area].extend(titles)
    return groups


def summarize_executed_tasks(
    tasks_dir: Path, max_recent: int = 10, root_id: str = "",
) -> str:
    """Return a short text summary of recently executed tasks in one dir."""
    if not tasks_dir.exists():
        return ""
    entries: List[Tuple[str, str]] = []
    area_counts: Dict[str, int] = {}
    for md in sorted(tasks_dir.glob("*.md")):
        fm = read_frontmatter(md)
        if not parse_bool(fm.get("executed")):
            continue
        iid = fm.get("id", md.stem)
        title = fm.get("title", fm.get("id", md.stem))
        entries.append((iid, f"  - [{iid}] {title}"))
        area = extract_area(iid, root_id)
        area_counts[area] = area_counts.get(area, 0) + 1
    if not entries:
        return ""
    total = len(entries)
    top_areas = sorted(area_counts.items(), key=lambda x: -x[1])[:12]
    area_str = " ".join(f"{a}:{c}" for a, c in top_areas)
    shown = [line for _, line in entries[-max_recent:]]
    header = f"  ({total} total — {area_str})"
    if total > max_recent:
        header += f"\n  (showing most recent {max_recent})"
    return header + "\n" + "\n".join(shown)


def summarize_all_executed_tasks(
    initiatives_base: Path, max_recent: int = 10, root_id: str = "",
) -> str:
    """Summarize recently executed tasks across ALL initiatives."""
    if not initiatives_base.exists():
        return ""
    entries: List[Tuple[str, str]] = []
    area_counts: Dict[str, int] = {}
    for init_dir in sorted(initiatives_base.iterdir()):
        td = init_dir / "tasks"
        if not td.is_dir():
            continue
        for md in sorted(td.glob("*.md")):
            fm = read_frontmatter(md)
            if not parse_bool(fm.get("executed")):
                continue
            iid = fm.get("id", md.stem)
            title = fm.get("title", fm.get("id", md.stem))
            entries.append((md.name, f"  - [{iid}] {title}"))
            area = extract_area(iid, root_id)
            area_counts[area] = area_counts.get(area, 0) + 1
    if not entries:
        return ""
    entries.sort(key=lambda e: e[0])
    labels = [e[1] for e in entries]
    total = len(labels)
    top_areas = sorted(area_counts.items(), key=lambda x: -x[1])[:12]
    area_str = " ".join(f"{a}:{c}" for a, c in top_areas)
    shown = labels[-max_recent:]
    header = f"  ({total} total — {area_str})"
    if total > max_recent:
        header += f"\n  (showing most recent {max_recent})"
    return header + "\n" + "\n".join(shown)


# ---------------------------------------------------------------------------
# Loading & ordering
# ---------------------------------------------------------------------------


def load_tasks(tasks_dir: Path) -> Dict[str, dict]:
    """Load task files into a dict keyed by task id."""
    tasks: Dict[str, dict] = {}
    for md in sorted(tasks_dir.glob("*.md")):
        if not md.is_file():
            continue
        fm = read_frontmatter(md)
        iid = fm.get("id") or md.stem
        # priority: 1=highest, 5=lowest, 0 or absent=unset (treated as 999)
        raw_priority = fm.get("priority", "0")
        try:
            priority_val = int(raw_priority)
        except (ValueError, TypeError):
            priority_val = 0
        tasks[iid] = {
            "id": iid,
            "title": strip_quotes(fm.get("title", iid)),
            "parent": fm.get("parent"),
            "deps": parse_deps(fm.get("deps", "[]")),
            "split": parse_bool(fm.get("split")),
            "planned": parse_bool(fm.get("planned")),
            "executed": parse_bool(fm.get("executed")),
            "depth": int(fm.get("depth", "0")),
            "priority": priority_val,
            "author": fm.get("author", ""),
            "path": md,
        }
    return tasks


def topological_sort(
    tasks: Dict[str, dict],
    already_executed: Set[str],
) -> List[str]:
    """Return task ids in dependency order.

    Raises ``ValueError`` if a cycle is detected.
    """
    executable = {
        iid: t
        for iid, t in tasks.items()
        if t["planned"]
        and not t["split"]
        and not t["executed"]
        and iid not in already_executed
    }
    if not executable:
        return []

    in_degree: Dict[str, int] = {iid: 0 for iid in executable}
    adj: Dict[str, List[str]] = {iid: [] for iid in executable}

    for iid, t in executable.items():
        for dep in t["deps"]:
            if dep in executable:
                in_degree[iid] += 1
                adj[dep].append(iid)

    # Sort by priority (1=highest first, 0/unset→999), then by id
    def _priority_key(iid: str) -> tuple:
        p = executable[iid].get("priority", 0) or 0
        return (p if p > 0 else 999, iid)

    queue = sorted(
        (iid for iid, d in in_degree.items() if d == 0),
        key=_priority_key,
    )
    result: List[str] = []

    while queue:
        node = queue.pop(0)
        result.append(node)
        newly_ready = []
        for dependent in adj[node]:
            in_degree[dependent] -= 1
            if in_degree[dependent] == 0:
                newly_ready.append(dependent)
        # Insert newly-ready items in priority order
        queue.extend(sorted(newly_ready, key=_priority_key))
        queue.sort(key=_priority_key)

    if len(result) != len(executable):
        remaining = set(executable) - set(result)
        raise ValueError(f"Dependency cycle detected involving: {remaining}")
    return result


# ---------------------------------------------------------------------------
# Mutation
# ---------------------------------------------------------------------------


def mark_executed(md_path: Path) -> None:
    """Set ``executed: true`` in the file's frontmatter."""
    text = md_path.read_text(encoding="utf-8")
    if re.search(r"^executed:\s*", text, flags=re.MULTILINE):
        updated = re.sub(
            r"^(executed:\s*)false\s*$", r"\1true",
            text, count=1, flags=re.MULTILINE,
        )
    else:
        lines = text.split("\n")
        inserted = False
        for i in range(1, len(lines)):
            if lines[i].strip() == "---":
                lines.insert(i, "executed: true")
                inserted = True
                break
        updated = "\n".join(lines) if inserted else text
    if updated != text:
        md_path.write_text(updated, encoding="utf-8")
