"""Documentation tools for generating logic maps, summaries, test scenarios, and review notes."""

import asyncio
import itertools
import re
from typing import Any

from mcp.server.fastmcp import FastMCP

from servicenow_mcp.auth import BasicAuthProvider
from servicenow_mcp.client import ServiceNowClient
from servicenow_mcp.config import Settings
from servicenow_mcp.decorators import tool_handler
from servicenow_mcp.policy import (
    INTERNAL_QUERY_LIMIT,
    check_table_access,
    mask_sensitive_fields,
)
from servicenow_mcp.tools.metadata import ARTIFACT_TABLES
from servicenow_mcp.utils import (
    ServiceNowQuery,
    format_response,
    validate_identifier,
)


# ── Shared helpers ────────────────────────────────────────────────────────


def _resolve_artifact_table(artifact_type: str, correlation_id: str) -> tuple[str, str | None]:
    """Resolve artifact_type to its table name, returning (table, None) or ("", error_response)."""
    table = ARTIFACT_TABLES.get(artifact_type)
    if not table:
        valid_types = ", ".join(sorted(ARTIFACT_TABLES.keys()))
        return "", format_response(
            data=None,
            correlation_id=correlation_id,
            status="error",
            error=f"Unknown artifact type '{artifact_type}'. Valid: {valid_types}",
        )
    return table, None


async def _fetch_artifact_script(client: ServiceNowClient, table: str, sys_id: str) -> tuple[dict[str, Any], str]:
    """Fetch an artifact record and return (masked_record, script_body)."""
    record = mask_sensitive_fields(await client.get_record(table, sys_id))
    script = record.get("script", "") or ""
    return record, script


def _classify_br_phases(br_records: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
    """Classify business rules by lifecycle phase."""
    phases: dict[str, list[dict[str, Any]]] = {}
    for br in br_records:
        when = br.get("when", "before")
        operations = []
        if br.get("action_insert") == "true":
            operations.append("insert")
        if br.get("action_update") == "true":
            operations.append("update")
        if br.get("action_delete") == "true":
            operations.append("delete")
        if not operations:
            operations = ["all"]

        for op in operations:
            phase_key = f"{when}_{op}"
            if phase_key not in phases:
                phases[phase_key] = []
            phases[phase_key].append(
                {
                    "type": "business_rule",
                    "sys_id": br.get("sys_id", ""),
                    "name": br.get("name", ""),
                    "order": br.get("order", ""),
                }
            )
    return phases


def _classify_client_script_phases(
    cs_records: list[dict[str, Any]],
    phases: dict[str, list[dict[str, Any]]],
) -> None:
    """Add client script entries into the phases dict, grouped by script type."""
    for cs in cs_records:
        cs_type = cs.get("type", "onChange")
        phase_key = f"client_{cs_type}"
        if phase_key not in phases:
            phases[phase_key] = []
        phases[phase_key].append(
            {
                "type": "client_script",
                "sys_id": cs.get("sys_id", ""),
                "name": cs.get("name", ""),
            }
        )


def _classify_ui_policies(uip_records: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Build UI policy entries for the phase map."""
    return [
        {
            "type": "ui_policy",
            "sys_id": p.get("sys_id", ""),
            "name": p.get("short_description", ""),
        }
        for p in uip_records
    ]


def _classify_ui_actions(uia_records: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Build UI action entries for the phase map."""
    return [
        {
            "type": "ui_action",
            "sys_id": a.get("sys_id", ""),
            "name": a.get("name", ""),
        }
        for a in uia_records
    ]


TOOL_NAMES: list[str] = [
    "docs_logic_map",
    "docs_artifact_summary",
    "docs_test_scenarios",
    "docs_review_notes",
]


def register_tools(mcp: FastMCP, settings: Settings, auth_provider: BasicAuthProvider) -> None:
    """Register documentation tools on the MCP server."""

    @mcp.tool()
    @tool_handler
    async def docs_logic_map(table: str, *, correlation_id: str) -> str:
        """Generate a lifecycle logic map of all automations on a table.

        Groups business rules, client scripts, UI policies, and UI actions by
        lifecycle phase (before_insert, after_update, display, async, etc.).

        Args:
            table: The table name to map.
        """
        validate_identifier(table)
        check_table_access(table)

        async with ServiceNowClient(settings, auth_provider) as client:
            # Fetch all four artifact types in parallel
            (
                br_result,
                cs_result,
                uip_result,
                uia_result,
            ) = await asyncio.gather(
                client.query_records(
                    "sys_script",
                    ServiceNowQuery().equals("collection", table).equals("active", "true").build(),
                    fields=[
                        "sys_id",
                        "name",
                        "when",
                        "action_insert",
                        "action_update",
                        "action_delete",
                        "order",
                        "active",
                    ],
                    limit=INTERNAL_QUERY_LIMIT,
                ),
                client.query_records(
                    "sys_script_client",
                    ServiceNowQuery().equals("table", table).equals("active", "true").build(),
                    fields=["sys_id", "name", "type", "active"],
                    limit=INTERNAL_QUERY_LIMIT,
                ),
                client.query_records(
                    "sys_ui_policy",
                    ServiceNowQuery().equals("table", table).equals("active", "true").build(),
                    fields=["sys_id", "short_description", "active"],
                    limit=INTERNAL_QUERY_LIMIT,
                ),
                client.query_records(
                    "sys_ui_action",
                    ServiceNowQuery().equals("table", table).equals("active", "true").build(),
                    fields=["sys_id", "name", "action_name", "active"],
                    limit=INTERNAL_QUERY_LIMIT,
                ),
            )

        # Build phase map from business rules
        phases = _classify_br_phases(br_result["records"])

        # Add client scripts, UI policies, and UI actions
        cs_records = cs_result["records"]
        _classify_client_script_phases(cs_records, phases)

        uip_records = uip_result["records"]
        if uip_records:
            phases["ui_policy"] = _classify_ui_policies(uip_records)

        uia_records = uia_result["records"]
        if uia_records:
            phases["ui_action"] = _classify_ui_actions(uia_records)

        total = len(br_result["records"]) + len(cs_records) + len(uip_records) + len(uia_records)

        return format_response(
            data={
                "table": table,
                "phases": phases,
                "total_automations": total,
            },
            correlation_id=correlation_id,
        )

    @mcp.tool()
    @tool_handler
    async def docs_artifact_summary(artifact_type: str, sys_id: str, *, correlation_id: str) -> str:
        """Generate a summary for a platform artifact including dependencies.

        Parses the artifact's script for referenced GlideRecord tables and uses
        code search to find what else references this artifact.

        Args:
            artifact_type: The artifact type (e.g. business_rule, script_include).
            sys_id: The sys_id of the artifact.
        """
        table, err = _resolve_artifact_table(artifact_type, correlation_id)
        if err:
            return err

        check_table_access(table)

        async with ServiceNowClient(settings, auth_provider) as client:
            record, script = await _fetch_artifact_script(client, table, sys_id)

            # Parse script for GlideRecord('table_name') references
            referenced_tables = _extract_gliderecord_tables(script)

            # Search for what references this artifact
            artifact_name = record.get("name", record.get("api_name", ""))
            referenced_by: list[dict[str, Any]] = []
            if artifact_name:
                try:
                    search_result = await client.code_search(term=artifact_name)
                    referenced_by = search_result.get("search_results", [])
                except Exception:
                    pass

        return format_response(
            data={
                "artifact": record,
                "referenced_tables": referenced_tables,
                "referenced_by": referenced_by,
                "summary": (
                    f"Artifact '{artifact_name}' references {len(referenced_tables)} table(s) "
                    f"and is referenced by {len(referenced_by)} other artifact(s)."
                ),
            },
            correlation_id=correlation_id,
        )

    @mcp.tool()
    @tool_handler
    async def docs_test_scenarios(artifact_type: str, sys_id: str, *, correlation_id: str) -> str:
        """Analyze an artifact's script and suggest test scenarios.

        Detects patterns like operation checks, conditional branches, role checks,
        and abort actions to generate relevant test scenario suggestions.

        Args:
            artifact_type: The artifact type (e.g. business_rule, script_include).
            sys_id: The sys_id of the artifact.
        """
        table, err = _resolve_artifact_table(artifact_type, correlation_id)
        if err:
            return err

        check_table_access(table)

        async with ServiceNowClient(settings, auth_provider) as client:
            record, script = await _fetch_artifact_script(client, table, sys_id)

        scenarios = _generate_test_scenarios(script)

        return format_response(
            data={
                "artifact": {
                    "name": record.get("name", ""),
                    "sys_id": sys_id,
                    "type": artifact_type,
                },
                "scenarios": scenarios,
                "scenario_count": len(scenarios),
            },
            correlation_id=correlation_id,
        )

    @mcp.tool()
    @tool_handler
    async def docs_review_notes(artifact_type: str, sys_id: str, *, correlation_id: str) -> str:
        """Scan an artifact's script for anti-patterns and generate review notes.

        Detects: GlideRecord in loop, hardcoded sys_ids, unbounded queries,
        and other common ServiceNow anti-patterns.

        Args:
            artifact_type: The artifact type (e.g. business_rule, script_include).
            sys_id: The sys_id of the artifact.
        """
        table, err = _resolve_artifact_table(artifact_type, correlation_id)
        if err:
            return err

        check_table_access(table)

        async with ServiceNowClient(settings, auth_provider) as client:
            record, script = await _fetch_artifact_script(client, table, sys_id)

        findings = _scan_for_anti_patterns(script)

        return format_response(
            data={
                "artifact": {
                    "name": record.get("name", ""),
                    "sys_id": sys_id,
                    "type": artifact_type,
                },
                "findings": findings,
                "finding_count": len(findings),
            },
            correlation_id=correlation_id,
        )


# ── Helper functions ──────────────────────────────────────────────────────


def _extract_gliderecord_tables(script: str) -> list[str]:
    """Extract table names from GlideRecord('table') and GlideRecordSecure('table') calls."""
    pattern = r"""(?:GlideRecord|GlideRecordSecure)\s*\(\s*['"]([^'"]+)['"]\s*\)"""
    matches = re.findall(pattern, script)
    # Deduplicate while preserving order
    seen: set[str] = set()
    result: list[str] = []
    for m in matches:
        if m not in seen:
            seen.add(m)
            result.append(m)
    return result


_WHILE_BLOCK_RE: re.Pattern[str] = re.compile(r"\bwhile\s*\(")
_FOR_BLOCK_RE: re.Pattern[str] = re.compile(r"\bfor\s*\(")
_GR_IN_BLOCK_RE: re.Pattern[str] = re.compile(r"new\s+GlideRecord(?:Secure)?\s*\(")

_SCENARIO_PATTERNS: list[tuple[re.Pattern[str], dict[str, str]]] = [
    (
        re.compile(r"current\.operation\(\)\s*==\s*['\"]insert['\"]"),
        {
            "scenario": "Test insert operation path",
            "description": "Create a new record and verify the insert-specific logic executes correctly.",
            "priority": "high",
        },
    ),
    (
        re.compile(r"current\.operation\(\)\s*==\s*['\"]update['\"]"),
        {
            "scenario": "Test update operation path",
            "description": "Update an existing record and verify the update-specific logic executes correctly.",
            "priority": "high",
        },
    ),
    (
        re.compile(r"current\.operation\(\)\s*==\s*['\"]delete['\"]"),
        {
            "scenario": "Test delete operation path",
            "description": "Delete a record and verify the delete-specific logic executes correctly.",
            "priority": "high",
        },
    ),
    (
        re.compile(r"current\.isNewRecord\(\)"),
        {
            "scenario": "Test new record vs existing record",
            "description": "Test behavior for both new and existing records since isNewRecord() is used.",
            "priority": "high",
        },
    ),
    (
        re.compile(r"\bif\s*\("),
        {
            "scenario": "Test condition branches",
            "description": "Verify each conditional branch (if/else) is tested with appropriate input values.",
            "priority": "medium",
        },
    ),
    (
        re.compile(r"setAbortAction\(true\)"),
        {
            "scenario": "Test abort action trigger",
            "description": "Verify conditions under which the operation is aborted and that the abort message is appropriate.",
            "priority": "high",
        },
    ),
]


def _generate_test_scenarios(script: str) -> list[dict[str, str]]:
    """Analyze script and generate test scenario suggestions."""
    scenarios: list[dict[str, str]] = []

    if not script.strip():
        scenarios.append(
            {
                "scenario": "Basic execution test",
                "description": "Verify the artifact executes without errors when triggered.",
                "priority": "high",
            }
        )
        return scenarios

    # Check data-driven patterns
    for pattern, scenario in _SCENARIO_PATTERNS:
        if pattern.search(script):
            scenarios.append(scenario)

    # Detect role checks (dynamic - can't be data-driven)
    role_matches = re.findall(r"gs\.hasRole\(['\"]([^'\"]+)['\"]\)", script)
    scenarios.extend(
        {
            "scenario": f"Test with role '{role}'",
            "description": f"Test behavior when user has and does not have the '{role}' role.",
            "priority": "medium",
        }
        for role in role_matches
    )

    # Detect GlideRecord queries (data dependency)
    tables = _extract_gliderecord_tables(script)
    if tables:
        scenarios.append(
            {
                "scenario": f"Test with dependent data ({', '.join(tables)})",
                "description": f"Ensure required records exist in {', '.join(tables)} before testing.",
                "priority": "medium",
            }
        )

    # Always add a generic scenario if nothing else detected
    if not scenarios:
        scenarios.append(
            {
                "scenario": "Basic execution test",
                "description": "Verify the artifact executes without errors when triggered.",
                "priority": "high",
            }
        )

    return scenarios


def _find_block_end(script: str, open_brace_idx: int) -> int:
    """Return the index of the matching closing brace, or end of script."""
    depth = 0
    for idx in range(open_brace_idx, len(script)):
        if script[idx] == "{":
            depth += 1
        elif script[idx] == "}":
            depth -= 1
            if depth == 0:
                return idx
    return len(script) - 1


def _find_matching_paren(script: str, open_paren_idx: int) -> int:
    """Return the index after the matching closing parenthesis, or end of script."""
    depth = 1
    pos = open_paren_idx + 1
    while pos < len(script) and depth > 0:
        if script[pos] == "(":
            depth += 1
        elif script[pos] == ")":
            depth -= 1
        pos += 1
    return pos


def _extract_loop_body(script: str, after_condition_idx: int) -> str:
    """Extract the body of a loop starting after its condition's closing paren."""
    body_start = after_condition_idx
    while body_start < len(script) and script[body_start] in " \t\r\n":
        body_start += 1

    if body_start >= len(script):
        return ""

    if script[body_start] == "{":
        block_end = _find_block_end(script, body_start)
        return script[body_start : block_end + 1]

    # Single statement (no braces) - find the next semicolon or newline
    stmt_end = len(script)
    for terminator in (";", "\n"):
        idx = script.find(terminator, body_start)
        if idx != -1:
            stmt_end = min(stmt_end, idx + 1)
    return script[body_start:stmt_end]


def _check_gr_in_loops(script: str) -> bool:
    """Return True if any while/for loop body contains a GlideRecord instantiation."""
    loop_matches = sorted(
        itertools.chain(_WHILE_BLOCK_RE.finditer(script), _FOR_BLOCK_RE.finditer(script)),
        key=lambda m: m.start(),
    )
    for m in loop_matches:
        open_paren = script.find("(", m.start())
        if open_paren == -1:
            continue  # pragma: no cover
        after_condition = _find_matching_paren(script, open_paren)
        body = _extract_loop_body(script, after_condition)
        if body and _GR_IN_BLOCK_RE.search(body):
            return True
    return False


def _scan_for_anti_patterns(script: str) -> list[dict[str, str]]:
    """Scan script for common ServiceNow anti-patterns."""
    findings: list[dict[str, str]] = []

    if not script.strip():
        return findings

    # 1. GlideRecord inside a loop (while/for containing new GlideRecord)
    if _check_gr_in_loops(script):
        findings.append(
            {
                "category": "gliderecord_in_loop",
                "severity": "warning",
                "message": (
                    "GlideRecord instantiated inside a loop. This is a major performance "
                    "concern - move the query outside the loop or use GlideAggregate."
                ),
            }
        )

    # 2. Hardcoded sys_ids (32-char hex strings)
    sysid_matches = re.findall(r"['\"]([0-9a-f]{32})['\"]", script)
    if sysid_matches:
        findings.append(
            {
                "category": "hardcoded_sys_id",
                "severity": "warning",
                "message": (
                    f"Found {len(sysid_matches)} hardcoded sys_id(s). Use system properties "
                    "or reference qualifiers instead for portability across instances."
                ),
            }
        )

    # 3. Unbounded GlideRecord query (query() without addQuery/addEncodedQuery/get)
    gr_blocks = re.findall(r"new\s+GlideRecord\s*\([^)]+\)\s*;(.{0,2000}?)\.query\(\)", script, re.DOTALL)
    for block in gr_blocks:
        if "addQuery" not in block and "addEncodedQuery" not in block and "get(" not in block:
            findings.append(
                {
                    "category": "unbounded_query",
                    "severity": "info",
                    "message": (
                        "GlideRecord.query() called without addQuery or addEncodedQuery. "
                        "This may return all records in the table. Add appropriate filters."
                    ),
                }
            )
            break  # Report once

    # 4. current.update() in a business rule
    if re.search(r"current\.update\(\)", script):
        findings.append(
            {
                "category": "current_update_in_br",
                "severity": "warning",
                "message": (
                    "current.update() called in script. In a business rule, this can cause "
                    "recursive execution. Use workflow: false or autoSysFields: false if intentional."
                ),
            }
        )

    return findings
