"""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,
    *,
    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"

    Args:
        command: The sf subcommand (e.g., "org list", "package installed list").
        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.
    """
    full_command = f"sf {command}"
    validate_sf_command(full_command)

    args = ["sf"] + command.split()
    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 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,
    )
    return data.get("result", [])
