"""Mirror coordinator Kanban batch progress into the in-thread task plan."""

from __future__ import annotations

import logging
import re
from typing import Any

_log = logging.getLogger(__name__)

_KANBAN_TERMINAL_DONE = frozenset({"done", "cancelled"})
_KANBAN_BLOCKED = frozenset({"blocked", "gave_up"})
_KANBAN_ACTIVE = frozenset({"in_progress", "claimed"})


def maybe_sync_task_plan_for_task(task_id: str) -> bool:
    """Update the origin thread's task plan from the active coordinator batch."""
    tid = str(task_id or "").strip()
    if not tid:
        return False
    try:
        import kanban_storage as ks
        from utils.kanban_coordinator import COORDINATOR_BATCH_ID_META, tasks_for_batch

        task = ks.get_task(tid)
        if not task:
            return False
        origin = str(task.get("origin_thread_id") or "").strip()
        meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
        batch_id = str(meta.get(COORDINATOR_BATCH_ID_META) or "").strip()
        if not origin or not batch_id:
            return False
        batch_tasks = tasks_for_batch(origin, batch_id)
        if not batch_tasks:
            return False
        return sync_task_plan_from_kanban_batch(origin, batch_tasks)
    except Exception:
        _log.debug("task plan kanban sync failed for %s", tid, exc_info=True)
        return False


def sync_task_plan_from_kanban_batch(
    thread_id: str,
    batch_tasks: list[dict[str, Any]],
) -> bool:
    """Map batch task statuses onto plan steps and persist when changed."""
    tid = str(thread_id or "").strip()
    if not tid or not batch_tasks:
        return False

    from chat_storage import ensure_encryption_ready, get_task_plan, set_task_plan
    from utils.task_plan import notify_task_plan_changed, validate_and_normalize

    ensure_encryption_ready()
    plan = get_task_plan(tid)
    if not isinstance(plan, dict) or not isinstance(plan.get("steps"), list):
        return False

    ordered_tasks = _topo_sort_batch_tasks(batch_tasks)
    if not ordered_tasks:
        return False

    steps = plan.get("steps")
    if not isinstance(steps, list):
        return False

    task_by_id = {
        str(t.get("id") or "").strip(): t
        for t in ordered_tasks
        if str(t.get("id") or "").strip()
    }
    assigned_task_ids = _assign_tasks_to_steps(steps, ordered_tasks, task_by_id)
    active_task_id = _active_batch_task_id(ordered_tasks)

    changed = False
    for step, kt_id in zip(steps, assigned_task_ids):
        if not isinstance(step, dict):
            continue
        meta = step.get("meta") if isinstance(step.get("meta"), dict) else {}
        if str(meta.get("role") or "").strip().lower() == "coordinator":
            continue
        task = task_by_id.get(kt_id or "") if kt_id else None
        if task is None:
            continue
        new_status, new_note = _plan_status_and_note(
            task,
            is_active=(active_task_id == kt_id),
        )
        if kt_id and meta.get("kanban_task_id") != kt_id:
            meta = dict(meta)
            meta["kanban_task_id"] = kt_id
            step["meta"] = meta
            changed = True
        if str(step.get("status") or "").strip().lower() != new_status:
            step["status"] = new_status
            changed = True
        if new_note and str(step.get("note") or "").strip() != new_note:
            step["note"] = new_note[:400]
            changed = True

    for step, kt_id in zip(steps, assigned_task_ids):
        if not isinstance(step, dict) or kt_id:
            continue
        meta = step.get("meta") if isinstance(step.get("meta"), dict) else {}
        if str(meta.get("role") or "").strip().lower() == "coordinator":
            continue
        if str(step.get("status") or "").strip().lower() == "in_progress":
            step["status"] = "pending"
            _note = "No matching kanban task in the active batch."
            if str(step.get("note") or "").strip() != _note:
                step["note"] = _note
                changed = True

    if not changed:
        return False

    try:
        normalized = validate_and_normalize(plan)
    except Exception:
        _log.debug("task plan normalize failed during kanban sync", exc_info=True)
        return False

    set_task_plan(tid, normalized)
    notify_task_plan_changed(tid)
    return True


def _token_set(text: str) -> set[str]:
    return set(re.findall(r"[a-z0-9]+", str(text or "").lower()))


def _title_overlap(step_text: str, task_title: str) -> float:
    a = _token_set(step_text)
    b = _token_set(task_title)
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)


def _assign_tasks_to_steps(
    steps: list[Any],
    ordered_tasks: list[dict[str, Any]],
    task_by_id: dict[str, dict[str, Any]],
) -> list[str]:
    """Return parallel kanban task ids per step (empty string when unmapped)."""
    used: set[str] = set()
    out: list[str] = []

    for step in steps:
        if not isinstance(step, dict):
            out.append("")
            continue
        meta = step.get("meta") if isinstance(step.get("meta"), dict) else {}
        if str(meta.get("role") or "").strip().lower() == "coordinator":
            out.append("")
            continue

        bound = str(meta.get("kanban_task_id") or "").strip()
        if bound and bound in task_by_id and bound not in used:
            used.add(bound)
            out.append(bound)
            continue

        step_text = str(step.get("text") or "")
        best_id = ""
        best_score = 0.12
        for task in ordered_tasks:
            kt_id = str(task.get("id") or "").strip()
            if not kt_id or kt_id in used:
                continue
            score = _title_overlap(step_text, str(task.get("title") or ""))
            if score > best_score:
                best_score = score
                best_id = kt_id
        if best_id:
            used.add(best_id)
        out.append(best_id)

    # Bind any still-unmapped tasks to remaining non-coordinator steps in order.
    leftover = [t for t in ordered_tasks if str(t.get("id") or "").strip() not in used]
    if leftover:
        li = 0
        for i, step in enumerate(steps):
            if out[i]:
                continue
            if not isinstance(step, dict):
                continue
            meta = step.get("meta") if isinstance(step.get("meta"), dict) else {}
            if str(meta.get("role") or "").strip().lower() == "coordinator":
                continue
            if li >= len(leftover):
                break
            kt_id = str(leftover[li].get("id") or "").strip()
            li += 1
            if kt_id:
                out[i] = kt_id
                used.add(kt_id)

    return out


def _topo_sort_batch_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Parents before children; tie-break by priority then id."""
    by_id = {
        str(t.get("id") or "").strip(): t
        for t in tasks
        if str(t.get("id") or "").strip()
    }
    if not by_id:
        return list(tasks)

    indegree: dict[str, int] = {tid: 0 for tid in by_id}
    children: dict[str, list[str]] = {tid: [] for tid in by_id}
    for tid, task in by_id.items():
        parents: list[str] = []
        pids = task.get("parent_ids")
        if isinstance(pids, list):
            parents = [str(p).strip() for p in pids if str(p).strip() in by_id]
        parent = str(task.get("parent_id") or "").strip()
        if parent and parent in by_id and parent not in parents:
            parents.append(parent)
        for p in parents:
            indegree[tid] += 1
            children[p].append(tid)

    ready = sorted(
        [tid for tid, deg in indegree.items() if deg == 0],
        key=lambda i: _task_sort_key(by_id[i]),
    )
    ordered_ids: list[str] = []
    while ready:
        tid = ready.pop(0)
        ordered_ids.append(tid)
        for child in sorted(children.get(tid) or [], key=lambda i: _task_sort_key(by_id[i])):
            indegree[child] -= 1
            if indegree[child] == 0:
                ready.append(child)
                ready.sort(key=lambda i: _task_sort_key(by_id[i]))

    for tid in sorted(by_id.keys(), key=lambda i: _task_sort_key(by_id[i])):
        if tid not in ordered_ids:
            ordered_ids.append(tid)
    return [by_id[i] for i in ordered_ids]


def _task_sort_key(task: dict[str, Any]) -> tuple[int, str]:
    try:
        pri = int(task.get("priority") or 0)
    except (TypeError, ValueError):
        pri = 0
    return (pri, str(task.get("id") or ""))


def _active_batch_task_id(tasks: list[dict[str, Any]]) -> str:
    for task in tasks:
        status = str(task.get("status") or "").strip().lower()
        if status in _KANBAN_ACTIVE:
            return str(task.get("id") or "").strip()
    for task in tasks:
        status = str(task.get("status") or "").strip().lower()
        if status in ("ready", "todo", "blocked"):
            return str(task.get("id") or "").strip()
    return ""


def _plan_status_and_note(
    task: dict[str, Any],
    *,
    is_active: bool,
) -> tuple[str, str]:
    from utils.task_plan import (
        STATUS_BLOCKED,
        STATUS_DONE,
        STATUS_IN_PROGRESS,
        STATUS_PENDING,
    )

    status = str(task.get("status") or "").strip().lower()
    note = _task_status_note(task)

    if status in _KANBAN_TERMINAL_DONE:
        return STATUS_DONE, note
    if status in _KANBAN_BLOCKED:
        return STATUS_BLOCKED, note
    if status in _KANBAN_ACTIVE or (is_active and status in ("ready", "todo")):
        return STATUS_IN_PROGRESS, note
    return STATUS_PENDING, ""


def _task_status_note(task: dict[str, Any]) -> str:
    status = str(task.get("status") or "").strip().lower()
    if status in _KANBAN_TERMINAL_DONE:
        try:
            import kanban_storage as ks

            runs = ks.list_runs(str(task.get("id") or ""), limit=3)
            for run in runs:
                summary = str(run.get("summary") or "").strip()
                if summary:
                    return summary[:400]
        except Exception:
            pass
        return str(task.get("title") or "")[:400]

    meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
    for key in ("next_action", "failure_reason"):
        val = str(meta.get(key) or "").strip()
        if val:
            return val[:400]
    try:
        import kanban_storage as ks

        runs = ks.list_runs(str(task.get("id") or ""), limit=2)
        for run in runs:
            na = str(run.get("next_action") or run.get("error") or "").strip()
            if na:
                return na[:400]
    except Exception:
        pass
    return ""
