"""Manager-owned state mutations for cross-project okstra orchestration."""
from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from pathlib import Path

from okstra_project import project_json_path, upsert_project_json

from okstra_ctl.jsonl import append_jsonl
from okstra_ctl.json_boundary import (
    JsonBoundaryError,
    load_owned_object,
    write_owned_object_atomic,
)

from .manager_paths import (
    ManagerPathError,
    canonical_manager_id,
    children_json_path,
    directives_jsonl_path,
    events_jsonl_path,
    manager_json_path,
    manager_root,
    projects_json_path,
    task_manifest_path,
)


class ManagerError(Exception):
    """Manager state validation or I/O failure."""


def _now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _write_json(path: Path, payload: dict) -> None:
    write_owned_object_atomic(path, payload, artifact="manager state")


def _read_json(path: Path) -> dict:
    if not path.is_file():
        raise ManagerError(f"manager file not found: {path}")
    try:
        data = load_owned_object(path, artifact="manager state")
    except JsonBoundaryError as exc:
        raise ManagerError(f"failed to parse {path}: {exc}") from exc
    if not isinstance(data, dict):
        raise ManagerError(f"expected JSON object in {path}")
    return data


def _read_json_default(path: Path, default: dict) -> dict:
    if not path.is_file():
        return default
    return _read_json(path)


def _project_entry(projects: dict, project_id: str) -> dict | None:
    for entry in projects.get("projects", []):
        if isinstance(entry, dict) and entry.get("projectId") == project_id:
            return entry
    return None


def _load_projects(home: Path, manager_id: str) -> dict:
    return _read_json_default(projects_json_path(home, manager_id), {"projects": []})


def init_manager(home: Path, manager_id: str, *, now: str | None = None) -> dict:
    try:
        canonical_id = canonical_manager_id(manager_id)
        root = manager_root(home, canonical_id)
    except ManagerPathError as exc:
        raise ManagerError(str(exc)) from exc
    path = manager_json_path(home, canonical_id)
    if path.is_file():
        return _read_json(path)
    payload = {
        "managerId": canonical_id,
        "schemaVersion": 1,
        "createdAt": now or _now_iso(),
    }
    root.mkdir(parents=True, exist_ok=True)
    _write_json(path, payload)
    return payload


def load_manager(home: Path, manager_id: str) -> dict:
    try:
        path = manager_json_path(home, canonical_manager_id(manager_id))
    except ManagerPathError as exc:
        raise ManagerError(str(exc)) from exc
    return _read_json(path)


def discover_projects(home: Path) -> list[dict]:
    base = Path(home) / "projects"
    if not base.is_dir():
        return []
    items: list[dict] = []
    for meta in sorted(base.glob("*/meta.json")):
        try:
            data = load_owned_object(meta, artifact="project metadata")
        except JsonBoundaryError:
            continue
        if isinstance(data, dict):
            items.append(
                {
                    "projectId": str(data.get("projectId") or ""),
                    "projectRoot": str(data.get("projectRoot") or ""),
                    "runCount": int(data.get("runCount", 0)),
                    "activeCount": int(data.get("activeCount", 0)),
                    "lastRunAt": str(data.get("lastRunAt") or ""),
                }
            )
    return [item for item in items if item["projectId"]]


def link_project(
    home: Path,
    manager_id: str,
    project_id: str,
    project_root: Path,
    *,
    role: str,
    tags: list[str],
    now: str | None = None,
    ) -> dict:
    load_manager(home, manager_id)
    root = Path(project_root).resolve()
    if not root.is_dir():
        raise ManagerError(f"projectRoot is not a directory: {root}")
    project_json = project_json_path(root)
    if project_json.is_file():
        try:
            existing = load_owned_object(project_json, artifact="project metadata")
        except JsonBoundaryError as exc:
            raise ManagerError(f"failed to parse {project_json}: {exc}") from exc
        existing_id = str(existing.get("projectId") or "")
        if existing_id != project_id:
            raise ManagerError(
                f"projectId mismatch: {project_json} has {existing_id!r}, expected {project_id!r}"
            )
    else:
        try:
            upsert_project_json(root, project_id, now=now)
        except Exception as exc:
            raise ManagerError(str(exc)) from exc
    payload = _load_projects(home, manager_id)
    entry = _project_entry(payload, project_id)
    next_entry = {
        "projectId": project_id,
        "projectRoot": str(root),
        "role": role,
        "tags": list(tags),
        "linkedAt": now or _now_iso(),
    }
    if entry is None:
        payload["projects"].append(next_entry)
    else:
        entry.update(next_entry)
    payload["projects"].sort(key=lambda item: item.get("projectId", ""))
    _write_json(projects_json_path(home, manager_id), payload)
    return payload


def create_task_group(home: Path, manager_id: str, task_group: str, *, now: str | None = None) -> dict:
    load_manager(home, manager_id)
    group_dir = task_manifest_path(home, manager_id, task_group, "group-marker").parents[1]
    group_dir.mkdir(parents=True, exist_ok=True)
    return {"managerId": manager_id, "taskGroup": task_group, "createdAt": now or _now_iso()}


def create_task(
    home: Path,
    manager_id: str,
    task_group: str,
    task_id: str,
    child_specs: list[dict],
    *,
    objective: str = "",
    common_brief_path: str = "",
    progress_mode: str = "manual",
    now: str | None = None,
) -> dict:
    projects = _load_projects(home, manager_id)
    when = now or _now_iso()
    manifest = {
        "managerId": manager_id,
        "taskGroup": task_group,
        "taskId": task_id,
        "progressMode": progress_mode,
        "commonBriefPath": common_brief_path,
        "objective": objective,
        "createdAt": when,
    }
    children = []
    for spec in child_specs:
        project_id = str(spec.get("projectId") or "")
        child_task_id = str(spec.get("taskId") or task_id)
        if _project_entry(projects, project_id) is None:
            raise ManagerError(f"unknown project membership: {project_id}")
        children.append(
            {
                "projectId": project_id,
                "taskGroup": task_group,
                "taskId": child_task_id,
                "taskKey": f"{project_id}:{task_group}:{child_task_id}",
                "role": "",
                "tags": [],
                "assignment": "",
                "launch": {
                    "status": "planned",
                    "childLeadBackend": "auto",
                    "workerDispatchBackend": "subagent",
                },
            }
        )
    payload = {"manifest": manifest, "children": {"children": children}}
    _write_json(task_manifest_path(home, manager_id, task_group, task_id), manifest)
    _write_json(children_json_path(home, manager_id, task_group, task_id), payload["children"])
    append_jsonl(
        events_jsonl_path(home, manager_id, task_group, task_id),
        {"event": "task-created", "createdAt": when},
    )
    return payload


def _load_children(home: Path, manager_id: str, task_group: str, task_id: str) -> dict:
    return _read_json_default(children_json_path(home, manager_id, task_group, task_id), {"children": []})


def _child_task_key(project_id: str, task_group: str, child_task_id: str) -> str:
    return f"{project_id}:{task_group}:{child_task_id}"


def _matches_child_task(child: dict, project_id: str, task_group: str, child_task_id: str) -> bool:
    target_key = _child_task_key(project_id, task_group, child_task_id)
    if child.get("taskKey") == target_key:
        return True
    return (
        child.get("projectId") == project_id
        and child.get("taskGroup") == task_group
        and child.get("taskId") == child_task_id
    )


def _select_child_for_assignment(
    children: list,
    project_id: str,
    task_group: str,
    child_task_id: str | None,
) -> dict:
    if child_task_id is not None:
        for child in children:
            if isinstance(child, dict) and _matches_child_task(
                child,
                project_id,
                task_group,
                child_task_id,
            ):
                return child
        raise ManagerError(
            f"planned child not found for task key: {_child_task_key(project_id, task_group, child_task_id)}"
        )
    matches = [child for child in children if isinstance(child, dict) and child.get("projectId") == project_id]
    if len(matches) == 1:
        return matches[0]
    if len(matches) > 1:
        raise ManagerError(f"ambiguous child assignment for project: {project_id}; pass child_task_id")
    raise ManagerError(f"planned child not found for project: {project_id}")


def assign_child(
    home: Path,
    manager_id: str,
    task_group: str,
    task_id: str,
    project_id: str,
    *,
    role: str,
    tags: list[str],
    assignment: str,
    child_task_id: str | None = None,
) -> dict:
    payload = _load_children(home, manager_id, task_group, task_id)
    child = _select_child_for_assignment(payload["children"], project_id, task_group, child_task_id)
    child["role"] = role
    child["tags"] = list(tags)
    child["assignment"] = assignment
    _write_json(children_json_path(home, manager_id, task_group, task_id), payload)
    return payload


def append_directive(
    home: Path,
    manager_id: str,
    task_group: str,
    task_id: str,
    *,
    scope: str,
    body: str,
    project_id: str = "",
    now: str | None = None,
    source: str = "user",
) -> dict:
    if scope not in {"shared", "project"}:
        raise ManagerError("scope must be shared or project")
    if scope == "project" and not project_id:
        raise ManagerError("project directive requires project-id")
    row = {
        "scope": scope,
        "projectId": project_id if scope == "project" else "",
        "body": body,
        "createdAt": now or _now_iso(),
        "source": source,
    }
    append_jsonl(directives_jsonl_path(home, manager_id, task_group, task_id), row)
    return row
