"""Session lifecycle handlers: open_session, close_session, recover_context."""
from __future__ import annotations

import secrets
from datetime import datetime, timezone

from mcp.types import TextContent

from ... import __version__
from ...buffer import has_pending
from ...context import ServerContext
from ...models import GithubConfig, RepoShapeEntry, SessionState
from ...parsers import parse_owner_repo, parse_skill_version
# Late binding for monkey-patch interception (#82 Phase 2b): tests
# reassign attributes on `active_claude_github_mcp.server`, so handlers
# must look up github/classifier helpers through the package at call
# time rather than via early-bound `from ...github.foo import bar`.
from active_claude_github_mcp import server as _s
from ..helpers import fetch_project_toc_body, ok, resolve_local_skill_path
from ...utils import compose_session_summary


def _read_local_skill_version(project_dir) -> str | None:
    """Parse the local skill file's `**Version:**` header, or None on miss.

    Mirrors the resolve+parse flow used by check_skill_version (#60) so
    open_session reports the same version that drift checks would.
    """
    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 _read_latest_skill_version(token: str, public_repo: str) -> str | None:
    """Fetch the public mirror's skill file at main, parse its version.

    Returns None on any failure (missing repo, network, parse error).
    """
    try:
        public_owner, public_name = parse_owner_repo(public_repo)
    except ValueError:
        return None
    try:
        text = _s.get_file_content(
            token, public_owner, public_name, "main", "active-claude-github.md"
        )
    except _s.GitHubError:
        return None
    if not text:
        return None
    return parse_skill_version(text)


def _build_repo_shape_targets(
    config: GithubConfig,
) -> list[tuple[str, str]]:
    """Return [(role, owner/name)] for every managed repo to audit."""
    targets: list[tuple[str, str]] = []
    targets.append(("publish_target", config.repos.public))
    targets.append(("dev", config.repos.private))
    for related in config.related_repos:
        if related.relationship == "subsystem":
            targets.append(("subsystem", related.repo))
        elif related.relationship == "publish_target":
            targets.append(("subsystem_publish", related.repo))
    return targets


def _evaluate_compliance(
    role: str,
    path: str,
    visibility: str,
    config: GithubConfig,
) -> tuple[bool, str | None]:
    """Apply §Step 3.5 compliance rules to a single repo entry.

    Rules:
    - role=dev: must be PRIVATE. Anything else is drift.
    - role=publish_target / subsystem_publish: ABSENT or PRIVATE permitted
      pre-release; PUBLIC always permitted. Outside-org placement permitted
      only if `publish_target_outside_org=True` is set.
    - role=subsystem: visibility unconstrained (subsystem repos can be
      public or private depending on project's release state).
    - For all roles: ABSENT is non-compliant unless explicitly allowed
      above.

    "Pre-release" is defined as: `publish_target_outside_org` flag is
    irrelevant; the absence/private state is permitted regardless.
    Tightening this to "no public release tag" is left to a future
    refinement when the repo-shape audit gains release-history visibility.
    """
    if role == "dev":
        if visibility == "PRIVATE":
            return True, None
        if visibility == "ABSENT":
            return False, f"dev repo not found at {path}"
        return False, f"dev repo is {visibility}; must be PRIVATE"

    if role in ("publish_target", "subsystem_publish"):
        # Cross-owner check first (org-membership of owner)
        org_owner = config.owner.lower()
        path_owner = path.split("/", 1)[0].lower()
        if path_owner != org_owner and not config.publish_target_outside_org:
            return False, (
                f"{path} owner '{path_owner}' is not the project's org "
                f"'{org_owner}'; set publish_target_outside_org=true in "
                "github-config.json if intentional"
            )
        # Visibility check — PUBLIC, PRIVATE, ABSENT all allowed.
        # INTERNAL is a no-op for now (no release-state to compare against).
        if visibility in ("PUBLIC", "PRIVATE", "ABSENT", "INTERNAL"):
            return True, None
        return False, f"unexpected visibility {visibility} for {path}"

    if role == "subsystem":
        if visibility == "ABSENT":
            return False, f"subsystem repo not found at {path}"
        return True, None

    return False, f"unknown role {role!r} for {path}"


def _audit_repo_shape(
    token: str,
    config: GithubConfig,
) -> list[RepoShapeEntry]:
    """Query GitHub for each managed repo's visibility; evaluate compliance."""
    entries: list[RepoShapeEntry] = []
    for role, path in _build_repo_shape_targets(config):
        try:
            owner, name = parse_owner_repo(path)
        except ValueError:
            entries.append(
                RepoShapeEntry(
                    path=path,
                    role=role,
                    visibility="ABSENT",
                    compliant=False,
                    compliance_note=f"path {path!r} is not in owner/name form",
                )
            )
            continue
        try:
            visibility = _s.get_repo_visibility(token, owner, name)
        except _s.GitHubError:
            visibility = None
        if visibility is None:
            visibility = "ABSENT"
        compliant, note = _evaluate_compliance(role, path, visibility, config)
        entries.append(
            RepoShapeEntry(
                path=path,
                role=role,
                visibility=visibility,
                compliant=compliant,
                compliance_note=note,
            )
        )
    return entries


async def open_session(ctx: ServerContext) -> list[TextContent]:
    session_id = secrets.token_hex(8)
    timestamp = datetime.now(timezone.utc).isoformat()

    comments = _s.get_discussion_comments(
        ctx.github_token, ctx.config.session_lock_discussion_id, last_n=5
    )
    prior_clean = True
    prior_timestamp = None

    for comment in reversed(comments):
        body = comment["body"]
        if "**Session opened**" in body:
            prior_clean = False
            prior_timestamp = comment.get("createdAt")
            break
        if "**Session closed**" in body:
            prior_timestamp = comment.get("createdAt")
            break

    body = (
        f"**Session opened**\n"
        f"Time: {timestamp}\n"
        f"Session ID: {session_id}\n"
        f"Skill version: {__version__}"
    )
    _s.add_discussion_comment(
        ctx.github_token, ctx.config.session_lock_discussion_id, body
    )

    pending_notice = ""
    if has_pending(ctx.project_dir):
        pending_notice = " ⚠️ Pending buffer has unresolved operations."

    skill_version = _read_local_skill_version(ctx.project_dir)
    latest_skill_version = _read_latest_skill_version(
        ctx.github_token, ctx.config.repos.public
    )
    repo_shape = _audit_repo_shape(ctx.github_token, ctx.config)

    state = SessionState(
        session_id=session_id,
        prior_session_clean=prior_clean,
        prior_session_timestamp=prior_timestamp,
        project_discussion_id=ctx.config.project_discussion_id,
        session_lock_discussion_id=ctx.config.session_lock_discussion_id,
        category_architecture_id=ctx.config.discussion_categories.Architecture.id,
        category_ux_ui_id=ctx.config.discussion_categories.UX_UI.id,
        category_ideas_id=ctx.config.discussion_categories.Ideas.id,
        category_claude_use_only_id=ctx.config.discussion_categories.claude_use_only.id,
        project_board_id=ctx.config.project_board.id,
        account_type=ctx.config.account_type,
        skill_version=skill_version,
        mcp_server_version=__version__,
        latest_skill_version=latest_skill_version,
        repo_shape=repo_shape,
    )

    result = state.model_dump()
    result["pending_notice"] = pending_notice
    ctx.current_session = dict(result)
    return ok(result)


async def close_session(
    ctx: ServerContext,
    summary: str | None,
    skill_update_candidates: int,
) -> list[TextContent]:
    effective = summary.strip() if isinstance(summary, str) else ""
    if not effective:
        effective = compose_session_summary(
            ctx.activity,
            skill_update_candidates,
            has_pending(ctx.project_dir),
        )

    timestamp = datetime.now(timezone.utc).isoformat()
    body = (
        f"**Session closed**\n"
        f"Time: {timestamp}\n"
        f"Summary: {effective}\n"
        f"Skill update candidates: {skill_update_candidates}"
    )
    _s.add_discussion_comment(
        ctx.github_token, ctx.config.session_lock_discussion_id, body
    )
    return ok({"summary": effective})


async def recover_context(ctx: ServerContext, days: int) -> list[TextContent]:
    if ctx.current_session is None:
        await open_session(ctx)
    session_snapshot = ctx.current_session

    owner_name, private_repo = parse_owner_repo(ctx.config.repos.private)

    toc_body = fetch_project_toc_body(
        ctx.github_token, ctx.config.project_discussion_id
    )
    open_issues = _s.list_open_issues(ctx.github_token, owner_name, private_repo)
    recent_discussions = _s.list_recent_discussions(
        ctx.github_token, owner_name, private_repo, since_days=days
    )
    milestones = _s.list_milestones_with_progress(
        ctx.github_token, owner_name, private_repo
    )

    return ok(
        {
            "session": session_snapshot,
            "project_toc_body": toc_body,
            "open_issues": open_issues,
            "recent_discussions": recent_discussions,
            "milestones": milestones,
            # #98 — surface the typed multi-repo graph so Claude's
            # cross-repo decisions at session start don't rely on
            # memory or a second config read.
            "related_repos": [
                r.model_dump() for r in ctx.config.related_repos
            ],
        }
    )
