"""Pre-task role/session router (#206).

Classifies a declared task type against the active MCP profile/session and
returns a route result before any downstream mutation tools run.
"""

from __future__ import annotations
import os

ROUTE_ALLOWED = "allowed_current_session"
ROUTE_WRONG_ROLE = "wrong_role_stop"
ROUTE_TO_AUTHOR = "route_to_author_session"
ROUTE_TO_REVIEWER = "route_to_reviewer_session"
ROUTE_AMBIGUOUS = "ambiguous_task_stop"
ROUTE_INFRA_STOP = "infra_stop"

_CONFLICT_HEAD = b"<" * 7 + b" "
_CONFLICT_TAIL = b">" * 7 + b" "
_CONFLICT_SEPARATOR = b"=" * 7


def python_bytes_have_conflict_markers(content: bytes) -> bool:
    """Return True when *content* contains git merge-conflict marker lines."""
    for line in content.splitlines():
        stripped = line.rstrip(b"\r\n")
        if stripped.startswith(_CONFLICT_HEAD):
            return True
        if stripped.startswith(_CONFLICT_TAIL):
            return True
        if stripped == _CONFLICT_SEPARATOR:
            return True
    return False


def skip_python_scan_walk_root(project_root: str, walk_root: str) -> bool:
    """Skip venv/git/cache and sibling worktrees under orchestration checkout.

    When *project_root* is itself a worktree inside ``branches/``, still scan
    that tree — do not treat the ``branches`` path segment as a skip signal.
    """
    rel = os.path.relpath(walk_root, project_root)
    if rel == ".":
        return False
    head = rel.split(os.sep, 1)[0]
    if head in ("venv", ".git", ".pytest_cache"):
        return True
    if head == "branches":
        nested = os.path.join(project_root, "branches")
        if os.path.isdir(nested) and (
            walk_root == nested or walk_root.startswith(nested + os.sep)
        ):
            return True
    return False


REVIEWER_TASKS = frozenset({
    "review_pr",
    "blind_pr_queue_review",
    "pr_queue_cleanup",
    "pr-queue-cleanup",
    "request_changes_pr",
    "approve_pr",
})

MERGER_TASKS = frozenset({
    "merge_pr",
})

AUTHOR_TASKS = frozenset({
    "create_issue",
    "comment_issue",
    "close_issue",
    "claim_issue",
    "create_branch",
    "push_branch",
    "create_pr",
    "comment_pr",
    "address_pr_change_requests",
    "work_issue",
    "work-issue",
    "reconcile_landed_pr",
})

RECONCILER_TASKS = frozenset({
    "cleanup_merged_pr_branch",
    # #729: delete_branch is reconciler-owned (gitea.branch.delete is granted
    # only to the reconciler profile). Raw gitea_delete_branch redirects here to
    # the guarded gitea_cleanup_merged_pr_branch path (#514/#687).
    "delete_branch",
    # #745: post-merge moot reviewer-lease cleanup is reconciler-owned; the
    # apply path posts a terminal lease marker. Kept in step with
    # task_capability_map so map and router cannot disagree (#723 defect A).
    "cleanup_post_merge_moot_lease",
    "gitea_cleanup_post_merge_moot_lease",
    "reconcile_already_landed_pr",
    "reconcile_already_landed",
    "reconcile-landed-pr",
})

TASK_REQUIRED_ROLE = {
    "create_issue": "author",
    "comment_issue": "author",
    "close_issue": "author",
    "claim_issue": "author",
    "create_branch": "author",
    "push_branch": "author",
    "create_pr": "author",
    "comment_pr": "author",
    "address_pr_change_requests": "author",
    # #729: reconciler-owned; see RECONCILER_TASKS and task_capability_map.
    "delete_branch": "reconciler",
    "review_pr": "reviewer",
    "merge_pr": "merger",
    "blind_pr_queue_review": "reviewer",
    "pr_queue_cleanup": "reviewer",
    "pr-queue-cleanup": "reviewer",
    "request_changes_pr": "reviewer",
    "approve_pr": "reviewer",
    "work_issue": "author",
    "work-issue": "author",
    "reconcile_landed_pr": "author",
    "reconcile_already_landed_pr": "reconciler",
    "reconcile_already_landed": "reconciler",
    "reconcile-landed-pr": "reconciler",
    "cleanup_merged_pr_branch": "reconciler",
    # #745: post-merge moot reviewer-lease cleanup (canonical task + tool alias).
    "cleanup_post_merge_moot_lease": "reconciler",
    "gitea_cleanup_post_merge_moot_lease": "reconciler",
    # #309: reconciler tasks close already-landed PRs/issues only.
    "reconcile_close_landed_pr": "reconciler",
    "reconcile_close_landed_issue": "reconciler",
    "reconcile_close_superseded_pr": "reconciler",
    "reconcile_close_satisfied_issue": "reconciler",
    "reconcile_create_followup_issue": "reconciler",
}

WRONG_ROLE_REVIEWER_MSG = (
    "Wrong role/session for reviewer task. Launch reviewer MCP namespace."
)

WRONG_ROLE_RECONCILER_MSG = (
    "Wrong role/session for reconciler task. Launch a reconciler-capable "
    "MCP namespace/profile with exact close capability."
)

WRONG_ROLE_MERGER_MSG = (
    "Wrong role/session for merger task. Launch merger MCP namespace."
)

_session_last_route: dict | None = None


def required_role_for_task(task_type: str) -> str | None:
    return TASK_REQUIRED_ROLE.get((task_type or "").strip())


def route_task_session(
    task_type: str,
    *,
    active_profile: str,
    active_role_kind: str,
    allowed_in_current_session: bool,
    runtime_switching_supported: bool = False,
) -> dict:
    """Return routing verdict for *task_type* under the active session."""
    task_type = (task_type or "").strip()
    required_role = required_role_for_task(task_type)
    if required_role is None:
        result = {
            "task_type": task_type,
            "required_role": None,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": ROUTE_AMBIGUOUS,
            "downstream_allowed": False,
            "reasons": [
                f"unknown task type '{task_type}'; cannot route session "
                "(fail closed)"
            ],
            "message": (
                "Ambiguous task type; relaunch with an explicit task before "
                "any tool use."
            ),
        }
        _record_route(result)
        return result

    if required_role == "reviewer":
        infra = assess_infra_stop()
        if infra["infra_stop"]:
            detail = "; ".join(infra.get("infra_stop_reasons") or [])
            message = (
                "infra_stop: Unresolved merge conflict or mid-merge state detected "
                f"in MCP runtime source ({detail}). Please resolve all conflicts "
                "manually, finish/abort the merge, and retry."
            )
            result = {
                "task_type": task_type,
                "required_role": required_role,
                "active_role": active_role_kind,
                "active_profile": active_profile,
                "route_result": ROUTE_INFRA_STOP,
                "downstream_allowed": False,
                "reasons": [message],
                "message": message,
                "infra_stop_assessment": infra,
            }
            _record_route(result)
            return result

    if allowed_in_current_session:
        result = {
            "task_type": task_type,
            "required_role": required_role,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": ROUTE_ALLOWED,
            "downstream_allowed": True,
            "reasons": [],
            "message": "Task role matches active session; proceed.",
        }
        _record_route(result)
        return result

    if required_role == "reviewer":
        result = {
            "task_type": task_type,
            "required_role": required_role,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": ROUTE_WRONG_ROLE,
            "downstream_allowed": False,
            "reasons": [
                WRONG_ROLE_REVIEWER_MSG,
                "Reviewer tasks cannot run in author-bound sessions.",
                "Static-profile mode does not permit in-place role switching.",
            ],
            "message": WRONG_ROLE_REVIEWER_MSG,
            "runtime_switching_supported": runtime_switching_supported,
            "profile_switch_blocked": not runtime_switching_supported,
        }
        _record_route(result)
        return result

    if required_role == "reconciler":
        result = {
            "task_type": task_type,
            "required_role": required_role,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": ROUTE_WRONG_ROLE,
            "downstream_allowed": False,
            "reasons": [
                WRONG_ROLE_RECONCILER_MSG,
                "Reconciler tasks cannot run in author or reviewer "
                "sessions without exact close capability.",
            ],
            "message": WRONG_ROLE_RECONCILER_MSG,
            "runtime_switching_supported": runtime_switching_supported,
            "profile_switch_blocked": not runtime_switching_supported,
        }
        _record_route(result)
        return result

    if required_role == "merger":
        result = {
            "task_type": task_type,
            "required_role": required_role,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": ROUTE_WRONG_ROLE,
            "downstream_allowed": False,
            "reasons": [
                WRONG_ROLE_MERGER_MSG,
                "Merger tasks cannot run in author or reviewer sessions.",
            ],
            "message": WRONG_ROLE_MERGER_MSG,
            "runtime_switching_supported": runtime_switching_supported,
            "profile_switch_blocked": not runtime_switching_supported,
        }
        _record_route(result)
        return result

    if required_role == "author":
        route = ROUTE_TO_AUTHOR
        message = (
            "Wrong role/session for author task. Launch author MCP namespace."
        )
        result = {
            "task_type": task_type,
            "required_role": required_role,
            "active_role": active_role_kind,
            "active_profile": active_profile,
            "route_result": route,
            "downstream_allowed": False,
            "reasons": [message],
            "message": message,
            "runtime_switching_supported": runtime_switching_supported,
            "profile_switch_blocked": not runtime_switching_supported,
        }
        _record_route(result)
        return result

    result = {
        "task_type": task_type,
        "required_role": required_role,
        "active_role": active_role_kind,
        "active_profile": active_profile,
        "route_result": ROUTE_AMBIGUOUS,
        "downstream_allowed": False,
        "reasons": ["unable to classify task role (fail closed)"],
        "message": "Ambiguous task type; stop before any mutation.",
    }
    _record_route(result)
    return result


def last_route() -> dict | None:
    return _session_last_route


def clear_route_state():
    global _session_last_route
    _session_last_route = None


def _record_route(result: dict):
    global _session_last_route
    _session_last_route = dict(result)


def sync_route_from_capability(capability: dict) -> None:
    """Align sticky route state with operation-scoped capability resolution (#228)."""
    capability = capability or {}
    task = (capability.get("requested_task") or "").strip()
    required_role = capability.get("required_role_kind")
    if not task or not required_role:
        return
    if capability.get("allowed_in_current_session"):
        _record_route({
            "task_type": task,
            "required_role": required_role,
            "active_role": required_role,
            "active_profile": capability.get("active_profile"),
            "route_result": ROUTE_ALLOWED,
            "downstream_allowed": True,
            "reasons": [],
            "message": (
                f"Operation-scoped task '{task}' resolved for current session; "
                "proceed."
            ),
        })
        return
    if required_role == "reviewer" and capability.get("stop_required"):
        _record_route({
            "task_type": task,
            "required_role": required_role,
            "active_role": capability.get("required_role_kind"),
            "active_profile": capability.get("active_profile"),
            "route_result": ROUTE_WRONG_ROLE,
            "downstream_allowed": False,
            "reasons": [WRONG_ROLE_REVIEWER_MSG],
            "message": WRONG_ROLE_REVIEWER_MSG,
        })


def check_author_mutation_after_reviewer_stop(mutation_task: str) -> tuple[bool, list[str]]:
    """Block author-side fallback after a reviewer wrong_role_stop (#206).

    An explicit operation-scoped author capability resolution for the same
    *mutation_task* clears the sticky reviewer denial (#228).
    """
    last = _session_last_route
    if not last:
        return True, []
    if (
        last.get("route_result") == ROUTE_ALLOWED
        and last.get("task_type") == mutation_task
        and last.get("required_role") == "author"
    ):
        return True, []
    if last.get("route_result") != ROUTE_WRONG_ROLE:
        return True, []
    if last.get("required_role") != "reviewer":
        return True, []
    if mutation_task in AUTHOR_TASKS:
        return False, [
            WRONG_ROLE_REVIEWER_MSG,
            "Author-side mutations are blocked after a reviewer-task "
            "wrong_role_stop unless the operator explicitly resolves the "
            "author task via gitea_resolve_task_capability.",
            f"Attempted fallback mutation: {mutation_task}",
        ]
    return True, []


def first_conflict_marker_path(project_root: str | None = None) -> str | None:
    """Return the first .py path containing a git conflict marker, or None."""
    root_dir = project_root or os.path.dirname(os.path.abspath(__file__))
    for root, dirs, files in os.walk(root_dir):
        if skip_python_scan_walk_root(root_dir, root):
            continue
        for file in files:
            if not file.endswith(".py"):
                continue
            file_path = os.path.join(root, file)
            try:
                with open(file_path, "rb") as f:
                    if python_bytes_have_conflict_markers(f.read()):
                        return file_path
            except OSError:
                pass
    return None


def _default_project_root() -> str:
    override = (os.environ.get("GITEA_MCP_PROJECT_ROOT") or "").strip()
    if override:
        return os.path.realpath(override)
    return os.path.dirname(os.path.abspath(__file__))


def assess_infra_stop(project_root: str | None = None) -> dict:
    """Recompute infra_stop from live git and source scan state (#285)."""
    root_dir = os.path.realpath(project_root or _default_project_root())
    reasons: list[str] = []
    mid_merge = False
    git_dir = os.path.join(root_dir, ".git")
    if os.path.isdir(git_dir):
        for marker in ("MERGE_HEAD", "rebase-merge", "rebase-apply"):
            marker_path = os.path.join(git_dir, marker)
            if os.path.exists(marker_path):
                mid_merge = True
                reasons.append(f"git state: {marker} present under {git_dir}")
    conflict_file = first_conflict_marker_path(root_dir)
    if conflict_file:
        reasons.append(
            f"conflict markers detected in {conflict_file} (project_root={root_dir})"
        )
    infra_stop = mid_merge or bool(conflict_file)
    return {
        "infra_stop": infra_stop,
        "project_root": root_dir,
        "conflict_file": conflict_file,
        "mid_merge": mid_merge,
        "infra_stop_reasons": reasons,
    }


def check_mid_merge(project_root: str | None = None) -> bool:
    """Return True if the repository is mid-merge, mid-rebase, or has conflict markers."""
    return assess_infra_stop(project_root)["infra_stop"]
