"""Safe wrapper around Salesforce CLI (sf).

Every command goes through the safety validator before execution.
All commands are logged for audit trail.
"""

import json
import logging
import subprocess
import shutil
from pathlib import Path
from typing import Any

from .safety import validate_sf_command, SafetyViolationError

logger = logging.getLogger(__name__)


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

    def __init__(self, message: str, command: str, exit_code: int, stderr: str):
        super().__init__(message)
        self.command = command
        self.exit_code = exit_code
        self.stderr = stderr


def check_sf_installed() -> dict[str, Any]:
    """Check if Salesforce CLI is installed and return version info.

    Returns dict with 'installed' (bool), 'version' (str or None),
    and 'path' (str or None).
    """
    sf_path = shutil.which("sf")
    if not sf_path:
        return {"installed": False, "version": None, "path": None}

    try:
        result = subprocess.run(
            ["sf", "version", "--json"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if result.returncode == 0:
            try:
                data = json.loads(result.stdout)
                version = data.get("cliVersion", result.stdout.strip())
            except json.JSONDecodeError:
                version = result.stdout.strip()
            return {"installed": True, "version": version, "path": sf_path}
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass

    return {"installed": True, "version": "unknown", "path": sf_path}


def run_sf(
    command: str | list[str],
    *,
    cwd: Path | str | None = None,
    timeout: int = 120,
    json_output: bool = True,
) -> dict[str, Any]:
    """Run an sf CLI command safely.

    The command string should NOT include 'sf' prefix -- it's added automatically.
    Example: run_sf("org list") runs "sf org list --json"
    Can also pass a list: run_sf(["project", "retrieve", "start", "--manifest", path])

    Args:
        command: The sf subcommand as string or list of args.
        cwd: Working directory for the command.
        timeout: Timeout in seconds.
        json_output: Whether to add --json flag and parse JSON response.

    Returns:
        Parsed JSON response as dict, or {"stdout": raw_output} if json_output=False.

    Raises:
        SafetyViolationError: If the command is not on the whitelist.
        SfCliError: If the command fails.
    """
    if isinstance(command, list):
        full_command = "sf " + " ".join(command)
        args = ["sf"] + command
    else:
        full_command = f"sf {command}"
        args = ["sf"] + command.split()
    validate_sf_command(full_command)
    if json_output and "--json" not in args:
        args.append("--json")

    logger.info("Running: %s (cwd=%s)", " ".join(args), cwd or ".")

    try:
        result = subprocess.run(
            args,
            capture_output=True,
            text=True,
            cwd=str(cwd) if cwd else None,
            timeout=timeout,
        )
    except subprocess.TimeoutExpired:
        raise SfCliError(
            f"Command timed out after {timeout}s: {full_command}",
            command=full_command,
            exit_code=-1,
            stderr="TimeoutExpired",
        )
    except FileNotFoundError:
        raise SfCliError(
            "Salesforce CLI (sf) is not installed or not on PATH. "
            "Install it from https://developer.salesforce.com/tools/salesforcecli",
            command=full_command,
            exit_code=-1,
            stderr="FileNotFoundError",
        )

    if result.returncode != 0:
        stderr = result.stderr.strip() or result.stdout.strip()
        raise SfCliError(
            f"sf command failed (exit {result.returncode}): {stderr}",
            command=full_command,
            exit_code=result.returncode,
            stderr=stderr,
        )

    if json_output:
        try:
            return json.loads(result.stdout)
        except json.JSONDecodeError:
            return {"stdout": result.stdout.strip()}

    return {"stdout": result.stdout.strip()}


def list_orgs(cwd: Path | str | None = None) -> list[dict[str, Any]]:
    """List all authorized Salesforce orgs."""
    data = run_sf("org list", cwd=cwd)
    result = data.get("result", {})
    orgs = []
    for category in ["nonScratchOrgs", "scratchOrgs"]:
        orgs.extend(result.get(category, []))
    return orgs


def display_org(
    alias: str, cwd: Path | str | None = None
) -> dict[str, Any]:
    """Get detailed info about a specific org."""
    return run_sf(f"org display --target-org {alias}", cwd=cwd)


def generate_project(
    name: str, cwd: Path | str | None = None
) -> dict[str, Any]:
    """Run sf project generate to create a standard SFDX project."""
    return run_sf(
        f"project generate --name {name} --template standard",
        cwd=cwd,
        json_output=False,
    )


def retrieve_metadata(
    manifest_path: Path | str,
    target_org: str,
    cwd: Path | str | None = None,
    timeout: int = 600,
) -> dict[str, Any]:
    """Run sf project retrieve start with a package.xml manifest.

    Returns the parsed JSON result. Timeout defaults to 10 minutes
    since large orgs can take a while.
    """
    manifest_str = str(manifest_path)
    cmd_list = [
        "project", "retrieve", "start",
        "--manifest", manifest_str,
        "--target-org", target_org,
    ]
    return run_sf(cmd_list, cwd=cwd, timeout=timeout)


def list_installed_packages(
    target_org: str, cwd: Path | str | None = None
) -> list[dict[str, Any]]:
    """List all installed packages in a Salesforce org.

    Returns a list of dicts with keys:
    SubscriberPackageName, SubscriberPackageNamespace,
    SubscriberPackageVersionId, SubscriberPackageVersionName,
    SubscriberPackageVersionNumber.
    """
    data = run_sf(
        f"package installed list --target-org {target_org}",
        cwd=cwd,
        timeout=60,
    )
    result = data.get("result") or []
    if isinstance(result, list):
        return result
    return []


def list_sobjects(
    target_org: str, cwd: Path | str | None = None
) -> list[str]:
    """List all sObject API names in a Salesforce org.

    Returns a flat list of sObject API names (e.g., 'csord__Order__c').
    """
    data = run_sf(
        f"sobject list --target-org {target_org}",
        cwd=cwd,
        timeout=60,
    )
    result = data.get("result") or []
    if isinstance(result, list):
        return [str(s) for s in result]
    return []


def describe_sobject(
    sobject_name: str,
    target_org: str,
    cwd: Path | str | None = None,
) -> dict[str, Any]:
    """Describe an sObject -- returns fields, type info, etc.

    The result includes:
    - fields: list of field descriptors
    - customSetting: bool
    - name, label, keyPrefix, ...
    """
    data = run_sf(
        f"sobject describe --sobject-name {sobject_name} "
        f"--target-org {target_org}",
        cwd=cwd,
        timeout=30,
    )
    return data.get("result", data)


def data_query(
    query: str,
    target_org: str,
    cwd: Path | str | None = None,
    timeout: int = 120,
) -> list[dict[str, Any]]:
    """Run a read-only SOQL query and return records.

    The query is validated against the safety policy before execution.
    """
    from .safety import validate_soql_query
    validate_soql_query(query)

    data = run_sf(
        ["data", "query", "--query", query, "--target-org", target_org],
        cwd=cwd,
        timeout=timeout,
    )
    result = data.get("result", {})
    records = result.get("records") or []
    if isinstance(records, list):
        return records
    return []


def lma_query(
    query: str,
    lma_org: str,
    cwd: Path | str | None = None,
    timeout: int = 120,
) -> list[dict[str, Any]]:
    """Run a read-only SOQL query against the LMA org.

    Allows Account and sfLma__* objects that are normally blocked
    on customer orgs. Safety is enforced by org-context-aware
    validation (target_org == lma_org).
    """
    from .safety import validate_soql_query
    validate_soql_query(query, target_org=lma_org, lma_org=lma_org)

    data = run_sf(
        ["data", "query", "--query", query, "--target-org", lma_org],
        cwd=cwd,
        timeout=timeout,
    )
    result = data.get("result", {})
    records = result.get("records") or []
    if isinstance(records, list):
        return records
    return []


def sosl_search(
    search_term: str,
    returning: str,
    target_org: str,
    cwd: Path | str | None = None,
    timeout: int = 60,
) -> list[dict[str, Any]]:
    """Run a SOSL search via ``sf data search``.

    Builds a SOSL query like:
        FIND {search_term} IN NAME FIELDS RETURNING Account(Id, Name)

    Returns the list of records from the search results.
    """
    sosl_query = f"FIND {{{search_term}}} IN NAME FIELDS RETURNING {returning}"

    data = run_sf(
        ["data", "search", "--query", sosl_query, "--target-org", target_org],
        cwd=cwd,
        timeout=timeout,
    )

    result = data.get("result", {})
    search_records = result.get("searchRecords") or []
    if isinstance(search_records, list):
        return search_records
    return []
