"""scan_customer_code -- Scan retrieved metadata for CS namespace references.

Produces a typed dependency inventory classifying references by type
(API_CALL, TRIGGER, OBJECT_DML, FIELD_REF, SOQL_REF, LWC_IMPORT, TEST_ONLY).
No risk labels -- risk is determined by the AI through cross-referencing.
"""

import json
import logging
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from ..utils import state
from ..utils.pe_helpers import DEFAULT_NAMESPACE_MAPPING

logger = logging.getLogger(__name__)

TOOL_DEFINITION = {
    "name": "scan_customer_code",
    "description": (
        "Scan retrieved customer metadata for CloudSense namespace references "
        "and classify each by reference type (API_CALL, TRIGGER, OBJECT_DML, "
        "FIELD_REF, SOQL_REF, LWC_IMPORT, TEST_ONLY). Produces a dependency "
        "inventory -- NOT a risk assessment. Risk is determined by the AI "
        "when cross-referencing with release notes or repo diffs. "
        "Requires retrieve_metadata first."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "account_name": {
                "type": "string",
                "description": "Customer account name (e.g. 'NBN Co Ltd').",
            },
            "namespaces": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Override which namespaces to scan for. If omitted, "
                    "uses all CS namespaces from the installed packages "
                    "in org-connection.json."
                ),
            },
            "working_directory": {
                "type": "string",
                "description": "Workspace directory. The AI passes this automatically.",
            },
        },
        "required": ["account_name"],
    },
}

PREVIEW_LIMIT = 30

_ALL_CS_NAMESPACES: set[str] = {
    entry["namespace"] for entry in DEFAULT_NAMESPACE_MAPPING
}

SCANNABLE_EXTENSIONS = {
    ".cls", ".trigger", ".page", ".component",
    ".js", ".html", ".xml",
}

FILE_TYPE_MAP = {
    ".cls": "ApexClass",
    ".trigger": "ApexTrigger",
    ".page": "ApexPage",
    ".component": "ApexComponent",
    ".js": "LWC/Aura",
    ".html": "LWC/Aura",
    ".xml": "Flow/Config",
}


def _make_slug(name: str) -> str:
    slug = name.lower().strip()
    slug = re.sub(r"[^a-z0-9]+", "-", slug)
    return slug.strip("-") or "unknown"


def _load_org_connection(working_dir: Path, slug: str) -> dict[str, Any] | None:
    conn_path = working_dir / "customers" / slug / "org-connection.json"
    if not conn_path.exists():
        return None
    try:
        return json.loads(conn_path.read_text())
    except (json.JSONDecodeError, OSError):
        return None


def _get_namespaces(conn: dict[str, Any] | None, overrides: list[str] | None) -> set[str]:
    """Get namespaces to scan for."""
    if overrides:
        return set(overrides)

    if conn:
        installed = conn.get("installed_packages", [])
        namespaces = set()
        for pkg in installed:
            ns = pkg.get("namespace", "").strip()
            if ns:
                namespaces.add(ns)
        if namespaces:
            return namespaces

    return _ALL_CS_NAMESPACES


def _build_patterns(namespaces: set[str]) -> dict[str, re.Pattern]:
    """Build regex patterns for each reference type."""
    ns_pattern = "|".join(re.escape(ns) for ns in sorted(namespaces))

    return {
        "API_CALL": re.compile(
            rf"(?:{ns_pattern})\.[\w]+\.[\w]+\s*\(",
            re.IGNORECASE,
        ),
        "TRIGGER": re.compile(
            rf"trigger\s+\w+\s+on\s+(?:{ns_pattern})__\w+",
            re.IGNORECASE,
        ),
        "OBJECT_DML": re.compile(
            rf"(?:insert|update|delete|upsert|merge)\s+.*?(?:{ns_pattern})__\w+",
            re.IGNORECASE,
        ),
        "SOQL_REF": re.compile(
            rf"(?:FROM|JOIN)\s+(?:{ns_pattern})__\w+",
            re.IGNORECASE,
        ),
        "FIELD_REF": re.compile(
            rf"(?:{ns_pattern})__\w+__(?:c|r)",
            re.IGNORECASE,
        ),
        "LWC_IMPORT": re.compile(
            rf"""import\s+.*?\s+from\s+['"](?:c/)?(?:{ns_pattern})""",
            re.IGNORECASE,
        ),
        "NAMESPACE_REF": re.compile(
            rf"(?:{ns_pattern})(?:__|\.)[\w]+",
            re.IGNORECASE,
        ),
    }


def _is_test_class(content: str) -> bool:
    """Detect if an Apex file is a test class."""
    return bool(re.search(r"@isTest", content, re.IGNORECASE))


def _extract_entity(match_text: str, ref_type: str, namespaces: set[str]) -> str:
    """Extract the CS entity name from a match."""
    ns_pattern = "|".join(re.escape(ns) for ns in sorted(namespaces))

    if ref_type == "API_CALL":
        m = re.search(rf"({ns_pattern})\.(\w+\.\w+)", match_text, re.IGNORECASE)
        if m:
            return f"{m.group(1)}.{m.group(2)}"

    if ref_type == "TRIGGER":
        m = re.search(rf"on\s+((?:{ns_pattern})__\w+)", match_text, re.IGNORECASE)
        if m:
            return m.group(1)

    if ref_type == "OBJECT_DML":
        m = re.search(rf"((?:{ns_pattern})__\w+)", match_text, re.IGNORECASE)
        if m:
            return m.group(1)

    if ref_type == "SOQL_REF":
        m = re.search(rf"(?:FROM|JOIN)\s+((?:{ns_pattern})__\w+)", match_text, re.IGNORECASE)
        if m:
            return m.group(1)

    if ref_type == "FIELD_REF":
        m = re.search(rf"((?:{ns_pattern})__\w+__(?:c|r))", match_text, re.IGNORECASE)
        if m:
            return m.group(1)

    if ref_type == "LWC_IMPORT":
        m = re.search(rf"(?:c/)?((?:{ns_pattern})\w*)", match_text, re.IGNORECASE)
        if m:
            return m.group(1)

    m = re.search(rf"((?:{ns_pattern})(?:__|\.)[\w]+)", match_text, re.IGNORECASE)
    if m:
        return m.group(1)

    return match_text.strip()[:80]


def _identify_namespace(match_text: str, namespaces: set[str]) -> str:
    """Identify which namespace a match belongs to."""
    text_lower = match_text.lower()
    for ns in namespaces:
        if ns.lower() in text_lower:
            return ns
    return "unknown"


def _scan_file(
    filepath: Path,
    patterns: dict[str, re.Pattern],
    namespaces: set[str],
    force_app_dir: Path,
) -> dict[str, Any] | None:
    """Scan a single file for CS namespace references."""
    try:
        content = filepath.read_text(errors="replace")
    except OSError:
        return None

    ext = filepath.suffix.lower()
    if ext == ".xml" and not filepath.name.endswith(".flow-meta.xml"):
        return None

    file_type = FILE_TYPE_MAP.get(ext, "Other")
    is_test = ext == ".cls" and _is_test_class(content)

    matches: list[dict[str, Any]] = []
    seen_lines: set[int] = set()
    lines = content.splitlines()

    ordered_types = ["API_CALL", "TRIGGER", "OBJECT_DML", "SOQL_REF",
                     "FIELD_REF", "LWC_IMPORT", "NAMESPACE_REF"]

    for ref_type in ordered_types:
        pattern = patterns.get(ref_type)
        if not pattern:
            continue

        for line_num, line in enumerate(lines, 1):
            if line_num in seen_lines and ref_type == "NAMESPACE_REF":
                continue

            for m in pattern.finditer(line):
                match_text = m.group(0).strip()
                entity = _extract_entity(match_text, ref_type, namespaces)
                ns = _identify_namespace(match_text, namespaces)
                actual_ref_type = "TEST_ONLY" if is_test else ref_type

                matches.append({
                    "line": line_num,
                    "namespace": ns,
                    "text": match_text[:120],
                    "ref_type": actual_ref_type,
                    "entity": entity,
                })

                if ref_type != "NAMESPACE_REF":
                    seen_lines.add(line_num)

    if not matches:
        return None

    try:
        rel_path = str(filepath.relative_to(force_app_dir.parent.parent))
    except ValueError:
        rel_path = str(filepath)

    return {
        "file": rel_path,
        "type": file_type,
        "is_test": is_test,
        "matches": matches,
    }


async def run(arguments: dict[str, Any]) -> str:
    """Execute the scan_customer_code tool."""
    state.set_working_dir(arguments.get("working_directory"))
    working_dir = state.get_working_dir()

    account_name = arguments.get("account_name", "").strip()
    namespace_overrides = arguments.get("namespaces")

    if not account_name:
        return "STATUS: MISSING_CUSTOMER\n\naccount_name is required."

    slug = _make_slug(account_name)
    conn = _load_org_connection(working_dir, slug)
    namespaces = _get_namespaces(conn, namespace_overrides)

    if not namespaces:
        return (
            "STATUS: NO_NAMESPACES\n\n"
            "No CloudSense namespaces to scan for. "
            "Either provide namespaces explicitly or connect an org "
            "with CS packages first."
        )

    metadata_dir = working_dir / "customers" / slug / "metadata"
    force_app = metadata_dir / "force-app" / "main" / "default"

    if not force_app.exists():
        return (
            "STATUS: NO_METADATA\n\n"
            f"No retrieved metadata found at `customers/{slug}/metadata/force-app/`.\n"
            "Run `retrieve_metadata` first to download customer code."
        )

    scannable_dirs = ["classes", "triggers", "pages", "components",
                      "aura", "lwc", "flows"]
    files_to_scan: list[Path] = []
    for dir_name in scannable_dirs:
        scan_dir = force_app / dir_name
        if scan_dir.exists():
            for f in scan_dir.rglob("*"):
                if f.is_file() and f.suffix.lower() in SCANNABLE_EXTENSIONS:
                    files_to_scan.append(f)

    if not files_to_scan:
        return (
            "STATUS: NO_METADATA\n\n"
            "No scannable files found in the retrieved metadata.\n"
            "Ensure `retrieve_metadata` was run for relevant types "
            "(ApexClass, ApexTrigger, Flow, etc.)."
        )

    patterns = _build_patterns(namespaces)

    all_references: list[dict[str, Any]] = []
    total_scanned = 0
    summary_by_ref_type: dict[str, int] = {}
    summary_by_namespace: dict[str, dict[str, int]] = {}
    all_entities: set[str] = set()

    for filepath in files_to_scan:
        total_scanned += 1
        result = _scan_file(filepath, patterns, namespaces, force_app)
        if not result:
            continue

        all_references.append(result)

        for match in result.get("matches", []):
            ref_type = match["ref_type"]
            ns = match["namespace"]
            entity = match["entity"]

            summary_by_ref_type[ref_type] = summary_by_ref_type.get(ref_type, 0) + 1
            all_entities.add(entity)

            if ns not in summary_by_namespace:
                summary_by_namespace[ns] = {"files": 0, "references": 0}
            summary_by_namespace[ns]["references"] += 1

    for ns in summary_by_namespace:
        ns_files = set()
        for ref in all_references:
            for match in ref.get("matches", []):
                if match["namespace"] == ns:
                    ns_files.add(ref["file"])
        summary_by_namespace[ns]["files"] = len(ns_files)

    scan_result = {
        "scanned_at": datetime.now(timezone.utc).isoformat(),
        "namespaces_scanned": sorted(namespaces),
        "total_files_scanned": total_scanned,
        "files_with_references": len(all_references),
        "summary_by_ref_type": dict(sorted(summary_by_ref_type.items())),
        "summary_by_namespace": dict(sorted(summary_by_namespace.items())),
        "cs_entities_referenced": sorted(all_entities),
        "references": all_references,
    }

    output_path = metadata_dir / "cs-references.json"
    output_path.write_text(json.dumps(scan_result, indent=2))

    if not all_references:
        return (
            "STATUS: NO_REFERENCES\n\n"
            f"Scanned {total_scanned:,} files -- no CloudSense namespace "
            f"references found.\n\n"
            f"Namespaces searched: {', '.join(sorted(namespaces))}\n"
            f"Saved: `customers/{slug}/metadata/cs-references.json`"
        )

    report = [f"STATUS: READY\n"]
    report.append("## Dependency Inventory\n")
    report.append(
        f"Scanned **{total_scanned:,}** files, "
        f"**{len(all_references):,}** contain CS references.\n"
    )

    report.append("### References by Type\n")
    for rt in ["API_CALL", "TRIGGER", "OBJECT_DML", "SOQL_REF",
                "FIELD_REF", "LWC_IMPORT", "TEST_ONLY", "NAMESPACE_REF"]:
        count = summary_by_ref_type.get(rt, 0)
        if count:
            report.append(f"- **{rt}**: {count:,}")

    report.append("\n### References by Namespace\n")
    for ns, data in sorted(summary_by_namespace.items()):
        report.append(f"- **{ns}**: {data['files']} files, {data['references']} references")

    report.append(f"\n### Distinct CS Entities Referenced: {len(all_entities)}\n")
    entity_preview = sorted(all_entities)[:PREVIEW_LIMIT]
    for entity in entity_preview:
        report.append(f"  - {entity}")
    if len(all_entities) > PREVIEW_LIMIT:
        report.append(f"  ... and {len(all_entities) - PREVIEW_LIMIT} more")

    report.append(f"\n### File Preview (first {min(len(all_references), PREVIEW_LIMIT)})\n")
    for ref in all_references[:PREVIEW_LIMIT]:
        match_count = len(ref.get("matches", []))
        test_label = " [TEST]" if ref.get("is_test") else ""
        report.append(f"- `{ref['file']}`{test_label}: {match_count} references")

    report.append(f"\nFull data: `customers/{slug}/metadata/cs-references.json`")
    report.append(
        "\nThis is a dependency inventory -- NOT a risk assessment. "
        "Cross-reference with release notes or repo diffs to determine impact."
    )

    return "\n".join(report)
