"""MCP Server for Salesforce Debug Log Analyzer.

Exposes tools for connecting to Salesforce orgs (via CLI auth or session ID),
capturing debug logs (trace flags / debug levels), and analyzing log content.
"""

import json
import logging
import subprocess
from typing import Optional

from mcp.server.fastmcp import FastMCP

from .sf_client import SalesforceClient
from .analyzer import scan_log, search_logs

logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s %(message)s")
logger = logging.getLogger("sf-mcp")

mcp = FastMCP(
    "sf-log-analyzer",
    instructions=(
        "Salesforce Debug Log Analyzer. Use sf_list_orgs / sf_connect_org to connect "
        "via CLI auth, or sf_connect with a raw session ID for subscriber orgs. "
        "Then use logging setup, log listing, scanning, and search tools."
    ),
)

# ── in-memory state ──────────────────────────────────────────────────────

_client: Optional[SalesforceClient] = None
_org_info: dict = {}
_log_cache: dict[str, str] = {}


def _require_client() -> SalesforceClient:
    if _client is None:
        raise RuntimeError(
            "Not connected to any Salesforce org. "
            "Use sf_connect_org (for CLI-authorized orgs) or sf_connect (for session ID) first."
        )
    return _client


# ═══════════════════════════════════════════════════════════════════════════
# CONNECTION TOOLS
# ═══════════════════════════════════════════════════════════════════════════


@mcp.tool()
def sf_list_orgs() -> str:
    """List all Salesforce orgs authorized via the sf CLI.

    Returns aliases, usernames, org type (sandbox/production/scratch),
    and connection status. Use this to find the right alias for sf_connect_org.
    """
    try:
        result = subprocess.run(
            ["sf", "org", "list", "--json"],
            capture_output=True, text=True, timeout=30,
        )
    except FileNotFoundError:
        return "Error: 'sf' CLI not found. Install it with: npm install -g @salesforce/cli"
    except subprocess.TimeoutExpired:
        return "Error: sf org list timed out."

    if result.returncode != 0:
        return f"Error listing orgs: {result.stderr.strip()}"

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return f"Error parsing CLI output: {result.stdout[:500]}"

    res = data.get("result", {})
    orgs = []

    for category, label in [
        ("nonScratchOrgs", "Production/Sandbox"),
        ("scratchOrgs", "Scratch"),
        ("sandboxes", "Sandbox"),
        ("devHubs", "DevHub"),
    ]:
        for org in res.get(category, []):
            alias = org.get("alias", "")
            username = org.get("username", "")
            org_id = org.get("orgId", "")
            connected = org.get("connectedStatus", org.get("status", ""))
            instance = org.get("instanceUrl", "")
            orgs.append(
                f"  {alias or '(no alias)':<25} {username:<45} {label:<20} {connected}  {instance}"
            )

    if not orgs:
        return "No authorized orgs found. Authorize one with: sf org login web -a <alias>"

    header = f"  {'Alias':<25} {'Username':<45} {'Type':<20} Status  Instance"
    separator = "  " + "-" * 130
    return f"Authorized Salesforce Orgs:\n{header}\n{separator}\n" + "\n".join(orgs)


@mcp.tool()
def sf_connect_org(alias: str) -> str:
    """Connect to a Salesforce org already authorized via sf CLI.

    Args:
        alias: The org alias or username (e.g. 'myDevSandbox', 'user@example.com').
    """
    global _client, _org_info, _log_cache

    try:
        result = subprocess.run(
            ["sf", "org", "display", "-o", alias, "--json"],
            capture_output=True, text=True, timeout=30,
        )
    except FileNotFoundError:
        return "Error: 'sf' CLI not found."
    except subprocess.TimeoutExpired:
        return "Error: sf org display timed out."

    if result.returncode != 0:
        return f"Error: Could not display org '{alias}'. Is it authorized?\n{result.stderr.strip()}"

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return f"Error parsing CLI output: {result.stdout[:500]}"

    r = data.get("result", {})
    access_token = r.get("accessToken")
    instance_url = r.get("instanceUrl")

    if not access_token or not instance_url:
        return (
            f"Error: Could not extract credentials for '{alias}'. "
            "The org may need re-authorization: sf org login web -a " + alias
        )

    try:
        client = SalesforceClient(access_token, instance_url)
        info = client.validate()
    except Exception as e:
        return f"Error validating connection: {e}"

    _client = client
    _log_cache = {}
    _org_info = {
        "alias": alias,
        "username": r.get("username", info.get("username", "")),
        "org_id": r.get("id", info.get("org_id", "")),
        "instance_url": instance_url,
        "connection_type": "cli_auth",
    }

    return (
        f"Connected to Salesforce org:\n"
        f"  Alias:        {_org_info['alias']}\n"
        f"  Username:     {_org_info['username']}\n"
        f"  Org ID:       {_org_info['org_id']}\n"
        f"  Instance:     {_org_info['instance_url']}\n"
        f"  API Calls:    {info.get('api_remaining', '?')}/{info.get('api_max', '?')} remaining"
    )


@mcp.tool()
def sf_connect(session_id: str, instance_url: str) -> str:
    """Connect to a Salesforce org using a raw session ID.

    Works for subscriber orgs (via ISV package access) and normal orgs.

    Args:
        session_id: The Salesforce session ID / access token.
        instance_url: The org's instance URL (e.g. https://myorg.my.salesforce.com).
    """
    global _client, _org_info, _log_cache

    try:
        client = SalesforceClient(session_id, instance_url)
        info = client.validate()
    except Exception as e:
        return f"Error connecting: {e}"

    _client = client
    _log_cache = {}
    _org_info = {
        "alias": "(session-id)",
        "username": info.get("username", ""),
        "org_id": info.get("org_id", ""),
        "instance_url": info.get("instance_url", instance_url),
        "connection_type": "session_id",
    }

    return (
        f"Connected to Salesforce org:\n"
        f"  Org ID:       {_org_info['org_id']}\n"
        f"  Username:     {_org_info['username']}\n"
        f"  Instance:     {_org_info['instance_url']}\n"
        f"  API Calls:    {info.get('api_remaining', '?')}/{info.get('api_max', '?')} remaining"
    )


@mcp.tool()
def sf_disconnect() -> str:
    """Disconnect from the current Salesforce org and clear all cached data."""
    global _client, _org_info, _log_cache
    old = _org_info.get("alias") or _org_info.get("org_id") or "(none)"
    _client = None
    _org_info = {}
    _log_cache = {}
    return f"Disconnected from {old}. Cache cleared."


# ═══════════════════════════════════════════════════════════════════════════
# LOG CAPTURE SETUP TOOLS
# ═══════════════════════════════════════════════════════════════════════════


@mcp.tool()
def sf_list_users() -> str:
    """List active users in the connected org.

    Returns user IDs, names, and usernames. Use the ID when creating
    trace flags or setting up logging.
    """
    client = _require_client()
    users = client.list_users()
    if not users:
        return "No active users found."

    lines = [f"  {'ID':<20} {'Name':<30} Username"]
    lines.append("  " + "-" * 90)
    for u in users:
        lines.append(f"  {u['id']:<20} {u['name']:<30} {u['username']}")
    return f"Active Users ({len(users)}):\n" + "\n".join(lines)


@mcp.tool()
def sf_list_debug_levels() -> str:
    """List existing debug levels in the connected org.

    Debug levels control what gets captured in logs (Apex detail, DB queries, etc.).
    Use an existing debug level ID with sf_create_trace_flag, or create a new one
    with sf_create_debug_level.
    """
    client = _require_client()
    levels = client.list_debug_levels()
    if not levels:
        return "No debug levels found. Create one with sf_create_debug_level."

    lines = [f"  {'ID':<20} {'Developer Name':<35} Label"]
    lines.append("  " + "-" * 70)
    for lv in levels:
        lines.append(f"  {lv['id']:<20} {lv['name']:<35} {lv['label']}")
    return f"Debug Levels ({len(levels)}):\n" + "\n".join(lines)


@mcp.tool()
def sf_create_debug_level(
    developer_name: str,
    label: str,
    apex_code: str = "FINE",
    apex_profiling: str = "NONE",
    callout: str = "INFO",
    database: str = "INFO",
    system_level: str = "INFO",
    validation: str = "INFO",
    visualforce: str = "INFO",
    workflow: str = "INFO",
) -> str:
    """Create a new debug level that controls log verbosity.

    Valid levels for each category: NONE, ERROR, WARN, INFO, DEBUG, FINE, FINER, FINEST.

    Args:
        developer_name: Unique API name (e.g. 'MCP_Debug_Detailed').
        label: Human-readable label.
        apex_code: Apex code detail level (default FINE).
        apex_profiling: Apex profiling level (default NONE).
        callout: Callout detail level (default INFO).
        database: Database/SOQL detail level (default INFO).
        system_level: System detail level (default INFO).
        validation: Validation rule detail level (default INFO).
        visualforce: Visualforce detail level (default INFO).
        workflow: Workflow detail level (default INFO).
    """
    client = _require_client()
    try:
        result = client.create_debug_level(
            developer_name=developer_name,
            label=label,
            apex_code=apex_code,
            apex_profiling=apex_profiling,
            callout=callout,
            database=database,
            system=system_level,
            validation=validation,
            visualforce=visualforce,
            workflow=workflow,
        )
        return (
            f"Debug level created:\n"
            f"  ID:    {result.get('id', 'unknown')}\n"
            f"  Name:  {developer_name}\n"
            f"  Apex:  {apex_code}  |  DB: {database}  |  Callout: {callout}\n"
            f"  System: {system_level}  |  Validation: {validation}  |  Workflow: {workflow}"
        )
    except Exception as e:
        return f"Error creating debug level: {e}"


@mcp.tool()
def sf_setup_logging(
    user_name_or_id: str,
    duration_minutes: int = 30,
    apex_code: str = "FINE",
    apex_profiling: str = "NONE",
    callout: str = "INFO",
    database: str = "INFO",
    system_level: str = "INFO",
    validation: str = "INFO",
    visualforce: str = "INFO",
    workflow: str = "INFO",
) -> str:
    """One-step setup: create a debug level + trace flag to start capturing logs.

    Finds the user, creates (or reuses) a debug level, and sets a trace flag.
    No need to visit Salesforce Setup.

    Args:
        user_name_or_id: User's name, username, or Salesforce ID to trace.
        duration_minutes: How long to capture logs (default 30, max 1440).
        apex_code: Apex code log level (default FINE).
        apex_profiling: Apex profiling level (default NONE).
        callout: Callout log level (default INFO).
        database: Database log level (default INFO).
        system_level: System log level (default INFO).
        validation: Validation log level (default INFO).
        visualforce: Visualforce log level (default INFO).
        workflow: Workflow log level (default INFO).
    """
    client = _require_client()

    users = client.list_users()
    search = user_name_or_id.lower()
    matched = None
    for u in users:
        if (u["id"].lower() == search
                or u["name"].lower() == search
                or u["username"].lower() == search
                or search in u["name"].lower()
                or search in u["username"].lower()):
            matched = u
            break

    if not matched:
        user_list = ", ".join(u["name"] for u in users[:10])
        return f"User '{user_name_or_id}' not found. Available users: {user_list}..."

    dev_name = "MCP_AutoDebug"
    label = "MCP Auto Debug Level"
    existing = client.list_debug_levels()
    debug_level_id = None
    for lv in existing:
        if lv["name"] == dev_name:
            debug_level_id = lv["id"]
            break

    if not debug_level_id:
        try:
            dl_result = client.create_debug_level(
                developer_name=dev_name,
                label=label,
                apex_code=apex_code,
                apex_profiling=apex_profiling,
                callout=callout,
                database=database,
                system=system_level,
                validation=validation,
                visualforce=visualforce,
                workflow=workflow,
            )
            debug_level_id = dl_result.get("id")
        except Exception as e:
            return f"Error creating debug level: {e}"

    try:
        tf_result = client.create_trace_flag(
            traced_entity_id=matched["id"],
            debug_level_id=debug_level_id,
            duration_minutes=duration_minutes,
        )
    except Exception as e:
        return f"Error creating trace flag: {e}"

    return (
        f"Logging enabled!\n"
        f"  User:      {matched['name']} ({matched['username']})\n"
        f"  Duration:  {duration_minutes} minutes\n"
        f"  Apex:      {apex_code}  |  DB: {database}  |  Callout: {callout}\n"
        f"  Trace ID:  {tf_result.get('id', 'unknown')}\n\n"
        f"Logs will now be captured. After the user performs actions, "
        f"use sf_list_logs to see captured logs."
    )


@mcp.tool()
def sf_list_trace_flags() -> str:
    """List active trace flags showing who is being traced and when it expires."""
    client = _require_client()
    flags = client.list_trace_flags()
    if not flags:
        return "No active trace flags. Use sf_setup_logging to start capturing."

    lines = [f"  {'ID':<20} {'User':<25} {'Debug Level':<25} {'Expires':<25} Type"]
    lines.append("  " + "-" * 110)
    for f in flags:
        lines.append(
            f"  {f['id']:<20} {f['traced_entity']:<25} "
            f"{f['debug_level']:<25} {f['expiration_date']:<25} {f['log_type']}"
        )
    return f"Trace Flags ({len(flags)}):\n" + "\n".join(lines)


@mcp.tool()
def sf_create_trace_flag(
    traced_entity_id: str,
    debug_level_id: str,
    duration_minutes: int = 60,
) -> str:
    """Create a trace flag for a specific user and debug level.

    For a simpler approach, use sf_setup_logging instead.

    Args:
        traced_entity_id: The Salesforce User ID to trace.
        debug_level_id: The Debug Level ID to use.
        duration_minutes: Duration in minutes (default 60).
    """
    client = _require_client()
    try:
        result = client.create_trace_flag(traced_entity_id, debug_level_id, duration_minutes)
        return f"Trace flag created: {result.get('id', 'unknown')} (expires in {duration_minutes} min)"
    except Exception as e:
        return f"Error creating trace flag: {e}"


@mcp.tool()
def sf_delete_trace_flag(flag_id: str) -> str:
    """Delete a trace flag to stop capturing logs for a user.

    Args:
        flag_id: The Trace Flag ID to delete.
    """
    client = _require_client()
    ok = client.delete_trace_flag(flag_id)
    if ok:
        return f"Trace flag {flag_id} deleted."
    return f"Failed to delete trace flag {flag_id}."


# ═══════════════════════════════════════════════════════════════════════════
# DEBUG LOG TOOLS
# ═══════════════════════════════════════════════════════════════════════════


@mcp.tool()
def sf_list_logs(
    operation: Optional[str] = None,
    user: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
    limit: int = 50,
) -> str:
    """List debug logs from the connected org.

    Args:
        operation: Filter by operation type (e.g. 'BatchApex', 'API', 'Trigger').
        user: Filter by user name.
        start_date: Filter logs after this date (YYYY-MM-DD).
        end_date: Filter logs before this date (YYYY-MM-DD).
        limit: Max logs to return (default 50).
    """
    client = _require_client()
    logs = client.list_logs(operation, user, start_date, end_date, limit)
    if not logs:
        return "No debug logs found matching the criteria."

    lines = [f"  {'ID':<20} {'Operation':<30} {'User':<20} {'Size':<10} {'Status':<10} Start Time"]
    lines.append("  " + "-" * 120)
    for l in logs:
        size_kb = f"{l['size'] / 1024:.0f}KB"
        time_str = l["start_time"][:19].replace("T", " ") if l["start_time"] else ""
        lines.append(
            f"  {l['id']:<20} {l['operation']:<30} {l['user']:<20} "
            f"{size_kb:<10} {l['status']:<10} {time_str}"
        )
    return f"Debug Logs ({len(logs)}):\n" + "\n".join(lines)


@mcp.tool()
def sf_get_log(log_id: str) -> str:
    """Fetch the full body of a specific debug log.

    Returns the raw log content. The LLM can then analyze it directly.

    Args:
        log_id: The ApexLog record ID.
    """
    client = _require_client()

    if log_id in _log_cache:
        body = _log_cache[log_id]
        return f"[Cached] Log {log_id} ({len(body)} chars, {len(body.splitlines())} lines):\n\n{body}"

    try:
        body = client.get_log_body(log_id)
    except Exception as e:
        return f"Error fetching log {log_id}: {e}"

    _log_cache[log_id] = body
    return f"Log {log_id} ({len(body)} chars, {len(body.splitlines())} lines):\n\n{body}"


@mcp.tool()
def sf_scan_log(log_id: str) -> str:
    """Scan a specific debug log for errors, exceptions, and governor limit issues.

    Runs pattern matching to detect CRITICAL/ERROR/WARNING issues without
    needing to read the full log.

    Args:
        log_id: The ApexLog record ID.
    """
    client = _require_client()

    if log_id in _log_cache:
        body = _log_cache[log_id]
    else:
        try:
            body = client.get_log_body(log_id)
            _log_cache[log_id] = body
        except Exception as e:
            return f"Error fetching log {log_id}: {e}"

    sr = scan_log(log_id, body)
    result = sr.to_dict()

    lines = [f"Scan Results for {log_id}:"]
    lines.append(f"  Severity: {result['max_severity']}  |  Issues: {result['has_issues']}")

    if result["namespaces"]:
        lines.append(f"  Namespaces detected: {', '.join(result['namespaces'])}")

    if result["findings"]:
        lines.append(f"\n  Findings ({len(result['findings'])}):")
        for f in result["findings"]:
            lines.append(f"    [{f['severity']}] Line {f['line']}: {f['label']}")
            lines.append(f"      {f['content'][:200]}")
    else:
        lines.append("\n  No error patterns detected.")

    if result["governor_usage"]:
        lines.append(f"\n  Governor Limit Warnings ({len(result['governor_usage'])}):")
        for g in result["governor_usage"]:
            lines.append(f"    {g['name']}: {g['used']}/{g['limit']} ({g['pct']}%)")

    return "\n".join(lines)


@mcp.tool()
def sf_scan_all_logs(
    operation: Optional[str] = None,
    user: Optional[str] = None,
    limit: int = 20,
) -> str:
    """Scan multiple debug logs for issues and return a summary.

    Downloads and scans each log, then provides an overview of which logs
    have errors and what the most common issues are.

    Args:
        operation: Filter by operation type.
        user: Filter by user name.
        limit: Max logs to scan (default 20, keep low to avoid timeouts).
    """
    client = _require_client()
    logs = client.list_logs(operation, user, limit=limit)
    if not logs:
        return "No logs found matching the criteria."

    results = []
    errors_list = []
    for log in logs:
        lid = log["id"]
        if log["size"] > 15_000_000:
            results.append({"log_id": lid, "skipped": True})
            continue
        try:
            if lid in _log_cache:
                body = _log_cache[lid]
            else:
                body = client.get_log_body(lid)
                _log_cache[lid] = body
            sr = scan_log(lid, body)
            d = sr.to_dict()
            results.append(d)
            if d["has_issues"]:
                errors_list.append((lid, log["operation"], d["max_severity"], len(d["findings"])))
        except Exception:
            results.append({"log_id": lid, "error": "Failed to download"})

    clean = sum(1 for r in results if not r.get("has_issues") and not r.get("error") and not r.get("skipped"))
    with_issues = sum(1 for r in results if r.get("has_issues"))
    failed = sum(1 for r in results if r.get("error"))
    skipped = sum(1 for r in results if r.get("skipped"))

    lines = [
        f"Scan Summary ({len(results)} logs):",
        f"  Clean: {clean}  |  With Issues: {with_issues}  |  Failed: {failed}  |  Skipped: {skipped}",
    ]

    if errors_list:
        lines.append(f"\n  Logs with issues:")
        for lid, op, sev, count in errors_list:
            lines.append(f"    [{sev}] {lid} - {op} ({count} findings)")
        lines.append(f"\n  Use sf_scan_log(log_id) for details on a specific log.")

    return "\n".join(lines)


@mcp.tool()
def sf_search_log(log_id: str, query: str) -> str:
    """Search within a debug log for specific text or patterns.

    Args:
        log_id: The ApexLog record ID.
        query: Text to search for (case-insensitive, space-separated terms are ANDed).
    """
    client = _require_client()

    if log_id in _log_cache:
        body = _log_cache[log_id]
    else:
        try:
            body = client.get_log_body(log_id)
            _log_cache[log_id] = body
        except Exception as e:
            return f"Error fetching log {log_id}: {e}"

    matches = search_logs(body, query)
    if not matches:
        return f"No matches for '{query}' in log {log_id}."

    lines = [f"Search results for '{query}' in {log_id} ({len(matches)} matches):"]
    for m in matches[:50]:
        lines.append(f"  L{m['line']}: {m['content']}")
    if len(matches) > 50:
        lines.append(f"  ... and {len(matches) - 50} more matches.")
    return "\n".join(lines)


# ═══════════════════════════════════════════════════════════════════════════

def main():
    """Entry point for the sf-log-analyzer console script."""
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
