"""
Thin wrappers around the Salesforce CLI (sf) for SOQL queries, Apex execution,
org configuration, and org resolution (fuzzy matching, session persistence).
"""

import json
import os
import re
import subprocess
import tempfile
from typing import Any, Dict, List, Optional

from ..session import get_session_org, set_session_org


class SfCliError(Exception):
    """Raised when an sf CLI command fails."""

    def __init__(self, message: str, stderr: str = "", stdout: str = ""):
        super().__init__(message)
        self.stderr = stderr
        self.stdout = stdout


def get_default_org() -> Optional[str]:
    """Return the default target-org alias from sf CLI config, or None."""
    try:
        result = subprocess.run(
            ["sf", "config", "get", "target-org", "--json"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if result.returncode == 0:
            data = json.loads(result.stdout)
            results = data.get("result", [])
            if isinstance(results, list) and results:
                return results[0].get("value")
            if isinstance(results, dict):
                return results.get("value")
    except Exception:
        pass
    return None


def list_orgs() -> List[Dict[str, Any]]:
    """Return a list of authenticated orgs from sf CLI."""
    try:
        result = subprocess.run(
            ["sf", "org", "list", "--json"],
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode == 0:
            data = json.loads(result.stdout)
            result_data = data.get("result", {})
            orgs = []
            for category in ("nonScratchOrgs", "scratchOrgs", "sandboxes", "other"):
                for org in result_data.get(category, []):
                    alias = org.get("alias", "")
                    username = org.get("username", "")
                    is_default = org.get("isDefaultUsername", False)
                    if alias or username:
                        orgs.append({
                            "alias": alias,
                            "username": username,
                            "isDefault": is_default,
                        })
            return orgs
    except Exception:
        pass
    return []


def resolve_default_org() -> Optional[str]:
    """Resolve the current project's default org via ``sf org display``.

    Falls back to ``sf config get target-org`` if ``sf org display`` fails.
    Returns the alias (preferred) or username, or None.
    """
    try:
        result = subprocess.run(
            ["sf", "org", "display", "--json"],
            capture_output=True,
            text=True,
            timeout=20,
        )
        if result.returncode == 0:
            data = json.loads(result.stdout)
            org_info = data.get("result", {})
            alias = org_info.get("alias") or org_info.get("username")
            if alias:
                return alias
    except Exception:
        pass
    return get_default_org()


def resolve_org_alias(hint: str) -> Dict[str, Any]:
    """Match a possibly partial org alias/hint against authenticated orgs.

    Returns a dict with:
      status   -- "exact", "matched", "ambiguous", or "no_match"
      alias    -- the resolved alias (set when status is exact/matched)
      candidates -- list of candidate orgs (set when ambiguous/no_match)
    """
    orgs = list_orgs()
    if not orgs:
        return {
            "status": "no_match",
            "alias": None,
            "candidates": [],
            "error": "No authenticated orgs found. Run 'sf org login web' to authorize an org.",
        }

    hint_lower = hint.lower()

    for org in orgs:
        if org["alias"].lower() == hint_lower or org["username"].lower() == hint_lower:
            return {"status": "exact", "alias": org["alias"] or org["username"], "candidates": []}

    matches = [
        org for org in orgs
        if hint_lower in org["alias"].lower() or hint_lower in org["username"].lower()
    ]

    if len(matches) == 1:
        resolved = matches[0]["alias"] or matches[0]["username"]
        return {"status": "matched", "alias": resolved, "candidates": []}

    if len(matches) > 1:
        return {
            "status": "ambiguous",
            "alias": None,
            "candidates": [{"alias": o["alias"], "username": o["username"]} for o in matches],
        }

    return {
        "status": "no_match",
        "alias": None,
        "candidates": [{"alias": o["alias"], "username": o["username"]} for o in orgs],
    }


def resolve_org(user_provided: Optional[str] = None) -> Dict[str, Any]:
    """Unified org resolution with the full priority chain.

    Priority: user-provided > session > workspace default > error.

    Returns a dict with:
      org_alias  -- the resolved alias to pass to CLI commands
      org_source -- "user", "session", or "default"

    On failure (ambiguous match, no orgs, etc.) returns:
      error      -- human-readable error message
      candidates -- list of candidate orgs (if applicable)
    """
    # 1. User explicitly provided an alias (or partial hint)
    if user_provided:
        resolution = resolve_org_alias(user_provided)

        if resolution["status"] in ("exact", "matched"):
            alias = resolution["alias"]
            set_session_org(alias)
            return {"org_alias": alias, "org_source": "user"}

        if resolution["status"] == "ambiguous":
            return {
                "error": (
                    f"The org hint '{user_provided}' matches multiple orgs. "
                    "Please specify one of the following:"
                ),
                "candidates": resolution["candidates"],
            }

        return {
            "error": (
                f"No org found matching '{user_provided}'. "
                "Available orgs:"
            ),
            "candidates": resolution["candidates"],
        }

    # 2. Session org from a previous call in this chat
    session_alias = get_session_org()
    if session_alias:
        return {"org_alias": session_alias, "org_source": "session"}

    # 3. Workspace default org
    default_alias = resolve_default_org()
    if default_alias:
        set_session_org(default_alias)
        return {"org_alias": default_alias, "org_source": "default"}

    # 4. Nothing worked -- return available orgs
    orgs = list_orgs()
    if orgs:
        return {
            "error": (
                "No default org is configured and no org was specified. "
                "Please specify an org_alias, or set a default with: "
                "sf config set target-org <alias>"
            ),
            "candidates": [{"alias": o["alias"], "username": o["username"]} for o in orgs],
        }

    return {
        "error": "No authenticated orgs found. Run 'sf org login web' to authorize an org.",
        "candidates": [],
    }


def run_soql(query: str, org_alias: Optional[str] = None) -> Dict[str, Any]:
    """
    Execute a SOQL query via `sf data query --json`.
    Returns the parsed JSON result dict with 'records', 'totalSize', etc.
    Raises SfCliError on failure.
    """
    cmd = ["sf", "data", "query", "--query", query, "--json"]
    if org_alias:
        cmd += ["--target-org", org_alias]

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    except subprocess.TimeoutExpired:
        raise SfCliError("SOQL query timed out after 120 seconds")

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        raise SfCliError(
            f"Failed to parse sf CLI output: {result.stdout[:500]}",
            stderr=result.stderr,
            stdout=result.stdout,
        )

    if result.returncode != 0:
        message = data.get("message", result.stderr or "Unknown error")
        raise SfCliError(f"SOQL query failed: {message}", stderr=result.stderr, stdout=result.stdout)

    return data.get("result", {})


def run_apex(
    apex_code: str,
    org_alias: Optional[str] = None,
    timeout_seconds: int = 120,
) -> "ApexResult":
    """
    Execute anonymous Apex via `sf apex run --file <tmpfile> --json`.
    Returns an ApexResult with parsed debug output.
    Raises SfCliError on failure.
    """
    tmp = tempfile.NamedTemporaryFile(
        mode="w", suffix=".apex", delete=False, encoding="utf-8"
    )
    try:
        tmp.write(apex_code)
        tmp.close()

        cmd = ["sf", "apex", "run", "--file", tmp.name, "--json"]
        if org_alias:
            cmd += ["--target-org", org_alias]

        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=timeout_seconds
            )
        except subprocess.TimeoutExpired:
            raise SfCliError(f"Apex execution timed out after {timeout_seconds} seconds")

        try:
            data = json.loads(result.stdout)
        except json.JSONDecodeError:
            raise SfCliError(
                f"Failed to parse sf CLI output: {result.stdout[:500]}",
                stderr=result.stderr,
                stdout=result.stdout,
            )

        if "result" not in data:
            error = data.get("message", result.stderr or "Unknown CLI error")
            raise SfCliError(f"Apex execution failed: {error}", stderr=result.stderr, stdout=result.stdout)

        apex_result = data["result"]

        if not apex_result.get("success", False):
            compile_problem = apex_result.get("compileProblem", "")
            exception_msg = apex_result.get("exceptionMessage", "")
            error = compile_problem or exception_msg or "Unknown Apex execution error"
            raise SfCliError(f"Apex execution failed: {error}", stderr=result.stderr, stdout=result.stdout)

        log_text = apex_result.get("logs", "")
        return ApexResult(success=True, log=log_text, raw=apex_result)
    finally:
        os.unlink(tmp.name)


class ApexResult:
    """Parsed result from an anonymous Apex execution."""

    def __init__(self, success: bool, log: str, raw: Dict[str, Any]):
        self.success = success
        self.log = log
        self.raw = raw

    def extract_chunks(self, prefix: str = "###CHUNK_") -> str:
        """
        Reassemble chunked output from System.debug log lines.
        Each USER_DEBUG line contains one chunk: ...USER_DEBUG|[N]|ERROR|###CHUNK_0###data
        Extracts data after the chunk marker on each line, sorts by index, and concatenates.
        """
        pattern = re.escape(prefix) + r"(\d+)###(.*)$"
        chunks = []
        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_tagged(self, start_tag: str, end_tag: str) -> List[str]:
        """
        Extract all text blocks between start_tag and end_tag markers in the log.
        Returns a list of extracted strings.
        """
        pattern = re.escape(start_tag) + r"(.*?)" + re.escape(end_tag)
        return re.findall(pattern, self.log, re.DOTALL)

    def extract_debug_lines(self, marker: str) -> List[str]:
        """Extract data from USER_DEBUG log lines containing a specific marker.

        Filters out source-code echo lines (Execute Anonymous:) that happen
        to contain the marker as a string literal.
        """
        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
