"""Infrastructure handlers: replay_pending, check_skill_version,
scaffold_project."""
from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Awaitable, Callable

from mcp.types import TextContent

from ...buffer import clear_pending, read_pending
from ...context import ServerContext
from ...github.projects import STATUS_OPTIONS
from ...parsers import parse_changelog, parse_owner_repo, parse_skill_version, semver_gt
# Late binding — see helpers.py docstring.
from active_claude_github_mcp import server as _s
from ..helpers import STANDARD_LABELS, ok, resolve_local_skill_path


CallTool = Callable[[str, dict], Awaitable[list[TextContent]]]

# Discussion categories every managed project needs. Created manually
# in the dev-repo UI (GitHub exposes no API for category creation);
# scaffold_project v2 pauses with instructions if any are missing.
REQUIRED_DISCUSSION_CATEGORIES: tuple[str, ...] = (
    "Project",
    "Architecture",
    "UX / UI",
    "Ideas",
    "claude-use-only",
)

# GitHub's default Status options on a freshly-created ProjectV2 board.
# If we see exactly this set on an existing board, it's safe to clobber
# with the canonical STATUS_OPTIONS.
_PROJECT_V2_DEFAULT_STATUS_OPTIONS: frozenset[str] = frozenset(
    {"Todo", "In Progress", "Done"}
)

_CONFIG_FILE_VERSION = "1.0.0"
_CONFIG_RELPATH = Path(".claude") / "github-config.json"


async def replay_pending(
    ctx: ServerContext,
    call_tool: CallTool,
) -> list[TextContent]:
    """Replay buffered operations (#57) through the server's dispatch."""
    entries = read_pending(ctx.project_dir)
    if not entries:
        return ok({"replayed": 0, "succeeded": 0, "failed": 0, "failures": []})

    # Clear up front; individual failures re-buffer via the normal path.
    clear_pending(ctx.project_dir)

    failures: list[dict] = []
    for entry in entries:
        tool_name = entry.get("tool", "")
        args = entry.get("args", {})
        try:
            result_content = await call_tool(tool_name, args)
        except Exception as exc:  # noqa: BLE001 — one bad entry mustn't
            # abort the whole replay
            failures.append({"tool": tool_name, "error": str(exc), "args": args})
            continue

        is_failure = False
        err_msg = ""
        if result_content:
            text = result_content[0].text
            try:
                parsed = json.loads(text)
                if isinstance(parsed, dict) and parsed.get("success") is False:
                    is_failure = True
                    err_msg = parsed.get("error", "unknown")
            except (ValueError, json.JSONDecodeError):
                pass  # plain-text success

        if is_failure:
            failures.append({"tool": tool_name, "error": err_msg, "args": args})

    replayed = len(entries)
    return ok(
        {
            "replayed": replayed,
            "succeeded": replayed - len(failures),
            "failed": len(failures),
            "failures": failures,
        }
    )


async def check_skill_version(ctx: ServerContext) -> list[TextContent]:
    """Report drift between the local skill file and its upstream copy (#60)."""
    local_path = resolve_local_skill_path(ctx.project_dir)
    installed_version: str | None = None
    if local_path is not None:
        try:
            installed_version = parse_skill_version(
                local_path.read_text(encoding="utf-8")
            )
        except OSError:
            installed_version = None

    latest_version: str | None = None
    upstream_changelog: list[dict] = []
    try:
        public_owner, public_repo = parse_owner_repo(ctx.config.repos.public)
        upstream_text = _s.get_file_content(
            ctx.github_token,
            public_owner,
            public_repo,
            "main",
            "active-claude-github.md",
        )
        if upstream_text:
            latest_version = parse_skill_version(upstream_text)
            upstream_changelog = parse_changelog(upstream_text)
    except (_s.GitHubError, ValueError):
        latest_version = None
        upstream_changelog = []

    drift = (
        bool(installed_version)
        and bool(latest_version)
        and installed_version != latest_version
    )

    if installed_version and latest_version and upstream_changelog:
        changelog_summary = [
            e
            for e in upstream_changelog
            if semver_gt(e["version"], installed_version)
        ]
    else:
        changelog_summary = upstream_changelog if drift else []

    return ok(
        {
            "installed_version": installed_version,
            "installed_path": (
                str(local_path.relative_to(ctx.project_dir))
                if local_path is not None
                else None
            ),
            "latest_version": latest_version,
            "drift": drift,
            "changelog_summary": changelog_summary,
        }
    )


def _ensure_repo(
    token: str,
    org: str,
    org_id: str,
    name: str,
    visibility: str,
    description: str,
) -> dict:
    """Return a repo descriptor, creating the repo if it doesn't exist.

    Idempotency anchor for scaffold_project v2: a re-run against an
    org where the repo pair already exists skips creation and returns
    the existing node IDs.

    Descriptor: {"id", "name_with_owner", "url", "created_now": bool}.
    """
    try:
        node_id = _s.get_repo_node_id(token, org, name)
        return {
            "id": node_id,
            "name_with_owner": f"{org}/{name}",
            "url": f"https://github.com/{org}/{name}",
            "created_now": False,
        }
    except _s.GitHubError:
        repo = _s.create_repository(
            token,
            owner_id=org_id,
            name=name,
            visibility=visibility,
            description=description,
        )
        return {
            "id": repo["id"],
            "name_with_owner": repo["name_with_owner"],
            "url": repo["url"],
            "created_now": True,
        }


def _seed_labels(token: str, repo_id: str) -> list[str]:
    """Create STANDARD_LABELS on a repo; swallow already-exists errors."""
    created: list[str] = []
    for label_name, color, label_desc in STANDARD_LABELS:
        try:
            _s.create_label(token, repo_id, label_name, color, label_desc)
            created.append(label_name)
        except (_s.GitHubError, ValueError):
            pass
    return created


def _read_existing_config(path: Path) -> dict | None:
    """Return the existing github-config.json contents, or None if absent/invalid."""
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None


def _try_parse_skill_version(project_dir: Path) -> str | None:
    """Parse the version header of a locally-installed skill file, or None."""
    local_path = resolve_local_skill_path(project_dir)
    if local_path is None:
        return None
    try:
        return parse_skill_version(local_path.read_text(encoding="utf-8"))
    except OSError:
        return None


def _build_config(
    *,
    org: str,
    public_repo: dict,
    dev_repo: dict,
    project: dict,
    status_field: dict,
    project_discussion_id: str,
    session_lock_discussion_id: str,
    categories: dict[str, dict],
    skill_version: str | None,
    existing: dict | None,
) -> dict:
    """Compose the github-config.json payload, merging with any existing file.

    Existing values take precedence for `bootstrapped_at` (original
    timestamp) and `skill_version_at_bootstrap` (first-install pin).
    Every other key is recomputed from the current run so a re-run
    refreshes stale IDs.
    """
    existing = existing or {}
    bootstrapped_at = existing.get("bootstrapped_at") or datetime.now(
        timezone.utc
    ).isoformat().replace("+00:00", "Z")
    pinned_skill_version = existing.get("skill_version_at_bootstrap", skill_version)

    config: dict[str, Any] = {
        "version": _CONFIG_FILE_VERSION,
        "skill": "active-claude-github",
        "account_type": "org",
        "owner": org,
        "repos": {
            "public": public_repo["name_with_owner"],
            "private": dev_repo["name_with_owner"],
        },
        "project_board": {
            "id": project["id"],
            "number": project["number"],
            "url": project["url"],
            "status_field_id": status_field["field_id"],
            "status_options": dict(status_field["options"]),
        },
        "project_discussion_id": project_discussion_id,
        "session_lock_discussion_id": session_lock_discussion_id,
        "discussion_categories": {
            name: {
                "id": categories[name]["id"],
                "isAnswerable": categories[name]["isAnswerable"],
            }
            for name in REQUIRED_DISCUSSION_CATEGORIES
            if name in categories
        },
        "skill_version_at_bootstrap": pinned_skill_version,
        "bootstrapped_at": bootstrapped_at,
    }
    return config


def _write_config(path: Path, config: dict) -> None:
    """Write github-config.json, creating parent directories as needed."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")


async def scaffold_project(
    ctx: ServerContext,
    org: str,
    project_name: str,
    description: str,
) -> list[TextContent]:
    """#81 — Scaffold a new managed project (v2: full bootstrap).

    Two-phase idempotent flow:

    * **Phase A** (first call against an empty org): create repo pair,
      seed labels, enable Discussions on dev repo, create ProjectV2
      board + Status field, link board to both repos. Pauses and
      returns manual-steps if Discussion categories are missing — the
      one GitHub feature with no API.
    * **Phase B** (re-run once categories are manually created):
      detect the required categories, seed Project + Session-Lock
      Discussions, write `.claude/github-config.json`.

    Every step inspects live state first and no-ops when already
    satisfied, so the tool is safe to re-run at any point.
    """
    if not org.strip() or not project_name.strip():
        raise ValueError("org and project_name must not be empty.")

    token = ctx.github_token

    # --- 1. Org exists? ---
    org_id = _s.get_organization_id(token, org)
    if org_id is None:
        return ok(
            {
                "created": False,
                "phase": "awaiting_org",
                "manual_steps": [
                    (
                        f"Organization '{org}' does not exist. "
                        "GitHub has no API for creating organisations. "
                        "Create it manually at "
                        "https://github.com/settings/organizations "
                        "then re-run scaffold_project."
                    )
                ],
            }
        )

    # --- 2. Ensure repo pair (idempotent) ---
    public_repo = _ensure_repo(
        token,
        org,
        org_id,
        project_name,
        "PUBLIC",
        description or f"{project_name} — public mirror",
    )
    dev_name = f"{project_name}-dev"
    dev_repo = _ensure_repo(
        token,
        org,
        org_id,
        dev_name,
        "PRIVATE",
        description or f"{project_name} — private dev, design, and archive",
    )

    # --- 3. Seed labels on both repos ---
    public_labels = _seed_labels(token, public_repo["id"])
    dev_labels = _seed_labels(token, dev_repo["id"])

    # --- 4. Enable Discussions on dev repo ---
    state = _s.get_repo_discussions_state(token, dev_repo["id"])
    discussions_enabled_now = False
    if not state["enabled"]:
        _s.enable_discussions(token, dev_repo["id"])
        discussions_enabled_now = True
        state = _s.get_repo_discussions_state(token, dev_repo["id"])

    # --- 5. Ensure ProjectV2 board + link both repos ---
    existing_projects = [
        p for p in _s.list_owner_projects(token, org_id) if p["title"] == project_name
    ]
    if existing_projects:
        project = existing_projects[0]
        project_created_now = False
    else:
        project = _s.create_projectv2(token, org_id, project_name)
        project_created_now = True

    _s.link_projectv2_to_repo(token, project["id"], public_repo["id"])
    _s.link_projectv2_to_repo(token, project["id"], dev_repo["id"])

    # --- 6. Set Status field options (read-diff-write, #6) ---
    status_field = _s.get_project_status_field(token, project["id"])
    current_names = set(status_field["options"].keys())
    desired_names = set(STATUS_OPTIONS)
    status_manual_note: str | None = None
    if current_names == desired_names:
        pass  # already correct
    elif project_created_now or current_names == _PROJECT_V2_DEFAULT_STATUS_OPTIONS:
        status_field = _s.update_projectv2_status_options(
            token, project["id"], status_field["field_id"], STATUS_OPTIONS
        )
    else:
        # Pre-existing board with custom Status options — don't clobber.
        status_manual_note = (
            f"Project board {project['url']} has custom Status options "
            f"({sorted(current_names)}). Expected: {list(STATUS_OPTIONS)}. "
            "Update them manually under the board's ⋯ → Settings → "
            "Fields → Status."
        )

    project_board_descriptor = {
        "id": project["id"],
        "number": project["number"],
        "url": project["url"],
        "status_field_id": status_field["field_id"],
        "status_options": dict(status_field["options"]),
        "created_now": project_created_now,
    }

    # --- 7. Discussion categories check ---
    missing_categories = [
        c for c in REQUIRED_DISCUSSION_CATEGORIES if c not in state["categories"]
    ]
    if missing_categories:
        manual_steps = [
            (
                f"Create Discussion categories on "
                f"{dev_repo['name_with_owner']} (Settings → Discussions "
                f"→ Categories) — missing: {missing_categories}. "
                "Required formats: Project (Announcement), Architecture "
                "(Q&A), UX / UI (Open-ended), Ideas (Open-ended), "
                "claude-use-only (Open-ended). After creating them, "
                "re-run scaffold_project to finish Phase B."
            )
        ]
        if status_manual_note:
            manual_steps.append(status_manual_note)
        return ok(
            {
                "created": False,
                "phase": "awaiting_categories",
                "public_repo": public_repo,
                "dev_repo": dev_repo,
                "labels_seeded": {
                    public_repo["name_with_owner"]: public_labels,
                    dev_repo["name_with_owner"]: dev_labels,
                },
                "discussions_enabled_now": discussions_enabled_now,
                "project_board": project_board_descriptor,
                "categories_present": sorted(state["categories"].keys()),
                "categories_missing": missing_categories,
                "manual_steps": manual_steps,
            }
        )

    # --- 8. Seed Project + Session-Lock Discussions ---
    config_path = ctx.project_dir / _CONFIG_RELPATH
    existing_config = _read_existing_config(config_path)
    project_category_id = state["categories"]["Project"]["id"]

    project_discussion_id = (
        existing_config.get("project_discussion_id") if existing_config else None
    )
    project_discussion_created_now = False
    if not project_discussion_id:
        disc = _s.create_discussion(
            token,
            dev_repo["id"],
            project_category_id,
            f"{project_name} — Project",
            (
                "Project Discussion (navigation hub).\n\n"
                "The server's `update_project_toc` will populate this "
                "post with the living Discussions + Issues TOC on the "
                "next turn that touches a tracked thread."
            ),
        )
        project_discussion_id = disc["id"]
        project_discussion_created_now = True

    session_lock_discussion_id = (
        existing_config.get("session_lock_discussion_id") if existing_config else None
    )
    session_lock_created_now = False
    if not session_lock_discussion_id:
        disc = _s.create_discussion(
            token,
            dev_repo["id"],
            project_category_id,
            f"{project_name} — Session Lock",
            (
                "Session Lock Discussion.\n\n"
                "`open_session` posts a session-opened comment here; "
                "`close_session` posts a session-closed comment with "
                "the session summary. One session at a time, serialised "
                "through this discussion."
            ),
        )
        session_lock_discussion_id = disc["id"]
        session_lock_created_now = True

    # --- 9. Write .claude/github-config.json ---
    config = _build_config(
        org=org,
        public_repo=public_repo,
        dev_repo=dev_repo,
        project=project,
        status_field=status_field,
        project_discussion_id=project_discussion_id,
        session_lock_discussion_id=session_lock_discussion_id,
        categories=state["categories"],
        skill_version=_try_parse_skill_version(ctx.project_dir),
        existing=existing_config,
    )
    _write_config(config_path, config)

    manual_steps: list[str] = []
    if status_manual_note:
        manual_steps.append(status_manual_note)

    return ok(
        {
            "created": True,
            "phase": "complete",
            "public_repo": public_repo,
            "dev_repo": dev_repo,
            "labels_seeded": {
                public_repo["name_with_owner"]: public_labels,
                dev_repo["name_with_owner"]: dev_labels,
            },
            "discussions_enabled_now": discussions_enabled_now,
            "project_board": project_board_descriptor,
            "project_discussion_id": project_discussion_id,
            "project_discussion_created_now": project_discussion_created_now,
            "session_lock_discussion_id": session_lock_discussion_id,
            "session_lock_created_now": session_lock_created_now,
            "discussion_categories": {
                name: state["categories"][name]
                for name in REQUIRED_DISCUSSION_CATEGORIES
            },
            "config_path": str(config_path.relative_to(ctx.project_dir)),
            "manual_steps": manual_steps,
        }
    )
