"""
Direct SOAP API client for Salesforce anonymous Apex execution.

Bypasses the sf CLI subprocess entirely -- authenticates once via
``sf org display --json`` and then calls the Apex SOAP API directly,
returning the debug log inline.  This eliminates the 2-5 second
Node.js startup overhead that ``sf apex run`` incurs per invocation.
"""

import html
import json
import logging
import re
import ssl
import subprocess
import time
import urllib.request
import xml.sax.saxutils
from typing import Any, Dict, List, Optional, Tuple

import certifi

from ..logging_config import log_event

logger = logging.getLogger("cloudsense_dx_mcp.sfdc.soap_client")


class SoapApexError(Exception):
    """Raised when the SOAP executeAnonymous call fails."""


_SOAP_ENVELOPE = """\
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:apex="http://soap.sforce.com/2006/08/apex">
  <soapenv:Header>
    <apex:SessionHeader>
      <apex:sessionId>{session_id}</apex:sessionId>
    </apex:SessionHeader>
    <apex:DebuggingHeader>
      <apex:categories>
        <apex:category>Apex_code</apex:category>
        <apex:level>ERROR</apex:level>
      </apex:categories>
    </apex:DebuggingHeader>
  </soapenv:Header>
  <soapenv:Body>
    <apex:executeAnonymous>
      <apex:String>{apex_body}</apex:String>
    </apex:executeAnonymous>
  </soapenv:Body>
</soapenv:Envelope>"""


def _build_ssl_context() -> ssl.SSLContext:
    """Build an SSL context using certifi's Mozilla CA bundle."""
    return ssl.create_default_context(cafile=certifi.where())


def get_org_credentials(org_alias: Optional[str] = None) -> Dict[str, str]:
    """Run ``sf org display --json`` once and return connection details.

    Returns dict with keys: access_token, instance_url, api_version, username.
    Raises SoapApexError on failure.
    """
    cmd = ["sf", "org", "display", "--json"]
    if org_alias:
        cmd += ["--target-org", org_alias]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    except FileNotFoundError:
        raise SoapApexError(
            "Salesforce CLI ('sf') not found on PATH. "
            "Install it from: https://developer.salesforce.com/tools/salesforcecli"
        )
    except subprocess.TimeoutExpired:
        raise SoapApexError("sf org display timed out after 30 seconds")

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        raise SoapApexError(f"Failed to parse sf org display output: {result.stdout[:500]}")

    if result.returncode != 0:
        msg = data.get("message", result.stderr or "Unknown error")
        raise SoapApexError(f"sf org display failed: {msg}")

    info = data.get("result", {})
    access_token = info.get("accessToken")
    instance_url = info.get("instanceUrl")
    api_version = info.get("apiVersion", "60.0")

    if not access_token or not instance_url:
        raise SoapApexError(
            "sf org display did not return accessToken/instanceUrl. "
            "Re-authorize with: sf org login web"
        )

    return {
        "access_token": access_token,
        "instance_url": instance_url,
        "api_version": api_version,
        "username": info.get("username", ""),
    }


def execute_apex(
    apex_code: str,
    instance_url: str,
    access_token: str,
    api_version: str = "60.0",
    timeout_seconds: int = 120,
    **log_context,
) -> "SoapApexResult":
    """Execute anonymous Apex via the SOAP API and return the result with debug log.

    This is functionally identical to ``sf apex run`` but avoids spawning
    a Node.js subprocess, cutting per-call overhead from ~15s to ~2s.

    Extra keyword arguments are passed through to log entries for traceability
    (e.g. request_id, template_name).
    """
    escaped_apex = xml.sax.saxutils.escape(apex_code)
    envelope = _SOAP_ENVELOPE.format(
        session_id=access_token,
        apex_body=escaped_apex,
    )

    url = f"{instance_url}/services/Soap/s/{api_version}"
    req = urllib.request.Request(
        url,
        data=envelope.encode("utf-8"),
        headers={
            "Content-Type": "text/xml; charset=UTF-8",
            "SOAPAction": "executeAnonymous",
        },
    )

    ctx = _build_ssl_context()
    call_start = time.monotonic()
    http_status = None
    try:
        with urllib.request.urlopen(req, timeout=timeout_seconds, context=ctx) as resp:
            http_status = resp.status
            body = resp.read().decode("utf-8")
        log_event(
            logger, "DEBUG",
            api="soap_execute_anonymous",
            url=url,
            duration_s=round(time.monotonic() - call_start, 2),
            http_status=http_status,
            response_size_bytes=len(body),
            success=True,
            **log_context,
        )
    except urllib.error.HTTPError as e:
        http_status = e.code
        error_body = e.read().decode("utf-8", errors="replace")[:1000]
        log_event(
            logger, "DEBUG",
            api="soap_execute_anonymous",
            url=url,
            duration_s=round(time.monotonic() - call_start, 2),
            http_status=http_status,
            success=False,
            error=f"HTTP {e.code}",
            **log_context,
        )
        raise SoapApexError(f"SOAP HTTP {e.code}: {error_body}")
    except urllib.error.URLError as e:
        log_event(
            logger, "DEBUG",
            api="soap_execute_anonymous",
            url=url,
            duration_s=round(time.monotonic() - call_start, 2),
            http_status=None,
            success=False,
            error=str(e.reason),
            **log_context,
        )
        raise SoapApexError(f"SOAP connection error: {e.reason}")
    except SoapApexError:
        raise
    except Exception as e:
        log_event(
            logger, "DEBUG",
            api="soap_execute_anonymous",
            url=url,
            duration_s=round(time.monotonic() - call_start, 2),
            http_status=None,
            success=False,
            error=str(e),
            **log_context,
        )
        raise SoapApexError(f"SOAP request failed: {e}")

    # Parse success/failure from the SOAP body
    success_match = re.search(r"<success>(true|false)</success>", body)
    compiled_match = re.search(r"<compiled>(true|false)</compiled>", body)
    success = success_match and success_match.group(1) == "true"
    compiled = compiled_match and compiled_match.group(1) == "true"

    if not compiled:
        problem = _extract_xml_text(body, "compileProblem") or "Unknown compile error"
        raise SoapApexError(f"Apex compilation failed: {problem}")

    if not success:
        exc_msg = _extract_xml_text(body, "exceptionMessage") or "Unknown runtime error"
        exc_trace = _extract_xml_text(body, "exceptionStackTrace") or ""
        raise SoapApexError(f"Apex execution failed: {exc_msg}\n{exc_trace}".strip())

    # Extract debug log from SOAP header
    log_match = re.search(r"<debugLog>(.*?)</debugLog>", body, re.DOTALL)
    log_text = html.unescape(log_match.group(1)) if log_match else ""

    return SoapApexResult(success=True, log=log_text, raw_response=body)


def _extract_xml_text(xml_str: str, tag: str) -> Optional[str]:
    """Extract text content from a simple XML tag (non-namespace-aware)."""
    m = re.search(rf"<{tag}>(.*?)</{tag}>", xml_str, re.DOTALL)
    if m:
        return html.unescape(m.group(1).strip())
    return None


class SoapApexResult:
    """Parsed result from a SOAP anonymous Apex execution.

    API-compatible with the existing ApexResult class so callers
    can use the same extract_chunks / extract_debug_lines methods.
    """

    def __init__(self, success: bool, log: str, raw_response: str):
        self.success = success
        self.log = log
        self.raw_response = raw_response

    def extract_chunks(self, prefix: str = "###CHUNK_") -> str:
        pattern = re.escape(prefix) + r"(\d+)###(.*)$"
        chunks: List[Tuple[int, str]] = []
        for line in self.log.splitlines():
            if prefix not in line:
                continue
            m = re.search(pattern, line)
            if m:
                chunks.append((int(m.group(1)), m.group(2)))
        if not chunks:
            return ""
        chunks.sort(key=lambda c: c[0])
        return "".join(data for _, data in chunks)

    def extract_debug_lines(self, marker: str) -> List[str]:
        lines = []
        for line in self.log.splitlines():
            if "USER_DEBUG" in line and marker in line:
                idx = line.index(marker) + len(marker)
                lines.append(line[idx:])
        return lines
