"""Salesforce lower-org deploy validation (Phase 3b)."""

from __future__ import annotations

import asyncio
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Protocol

from pydantic import BaseModel, Field

from packages.config import Settings, get_settings

_PROD_ALIAS_MARKERS = ("prod", "production")


class DeployResult(BaseModel):
    success: bool
    check_only: bool
    status: str
    message: str
    details: Dict[str, Any] = Field(default_factory=dict)


class SalesforceDeployer(Protocol):
    async def deploy_validate(self, *, source_dir: Path, jira_key: str) -> DeployResult: ...


class MockSalesforceDeployer:
    """Always-succeeding deployer for local/dev and CI."""

    def __init__(self) -> None:
        self.calls: List[Dict[str, object]] = []

    async def deploy_validate(self, *, source_dir: Path, jira_key: str) -> DeployResult:
        self.calls.append({"source_dir": str(source_dir), "jira_key": jira_key})
        return DeployResult(
            success=True,
            check_only=True,
            status="Succeeded",
            message=f"Mock deploy validate OK for {jira_key}",
            details={"provider": "mock", "source_dir": str(source_dir)},
        )


class CliSalesforceDeployer:
    """Run `sf project deploy start` against a lower-org alias (dry-run by default)."""

    def __init__(self, settings: Optional[Settings] = None) -> None:
        self.settings = settings or get_settings()

    async def deploy_validate(self, *, source_dir: Path, jira_key: str) -> DeployResult:
        alias = (self.settings.salesforce_org_alias or "").strip()
        if not alias:
            raise ValueError("SALESFORCE_ORG_ALIAS is required when SALESFORCE_PROVIDER=cli")
        if _looks_like_production(alias):
            raise ValueError(
                f"Refusing Salesforce deploy to production-like alias '{alias}' "
                "(v1 allows lower orgs only)"
            )
        if not source_dir.exists():
            raise ValueError(f"Deploy source dir missing: {source_dir}")

        check_only = bool(self.settings.salesforce_deploy_check_only)
        cmd = [
            "sf",
            "project",
            "deploy",
            "start",
            "--source-dir",
            str(source_dir),
            "--target-org",
            alias,
            "--json",
        ]
        if check_only:
            cmd.append("--dry-run")

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout_b, stderr_b = await proc.communicate()
        stdout = (stdout_b or b"").decode("utf-8", errors="replace")
        stderr = (stderr_b or b"").decode("utf-8", errors="replace")

        payload: Dict[str, Any] = {}
        try:
            payload = json.loads(stdout) if stdout.strip() else {}
        except json.JSONDecodeError:
            payload = {"raw_stdout": stdout}

        status_code = payload.get("status")
        result_block = payload.get("result") if isinstance(payload.get("result"), dict) else {}
        success_flag = result_block.get("success")
        if success_flag is None:
            success_flag = proc.returncode == 0 and status_code in (0, "0", None)

        deploy_status = (
            result_block.get("status")
            or result_block.get("deployStatus")
            or ("Succeeded" if success_flag else "Failed")
        )
        message = (
            result_block.get("errorMessage")
            or payload.get("message")
            or (stderr.strip() if not success_flag else f"Deploy validate completed for {jira_key}")
        )

        return DeployResult(
            success=bool(success_flag),
            check_only=check_only,
            status=str(deploy_status),
            message=str(message)[:2000],
            details={
                "provider": "cli",
                "org_alias": alias,
                "returncode": proc.returncode,
                "sf_status": status_code,
                "result": result_block or payload,
                "stderr": stderr[:2000] if stderr else "",
            },
        )


def _looks_like_production(alias: str) -> bool:
    lowered = alias.lower().replace("-", "").replace("_", "")
    return any(marker in lowered for marker in _PROD_ALIAS_MARKERS)


_mock_singleton: Optional[MockSalesforceDeployer] = None


def get_salesforce_deployer(settings: Optional[Settings] = None) -> SalesforceDeployer:
    global _mock_singleton
    settings = settings or get_settings()
    if settings.salesforce_provider == "cli":
        return CliSalesforceDeployer(settings)
    if _mock_singleton is None:
        _mock_singleton = MockSalesforceDeployer()
    return _mock_singleton


def reset_mock_salesforce() -> MockSalesforceDeployer:
    global _mock_singleton
    _mock_singleton = MockSalesforceDeployer()
    return _mock_singleton


class ApexTestResult(BaseModel):
    success: bool
    tests_run: int = 0
    tests_passed: int = 0
    failures: List[str] = Field(default_factory=list)
    details: Dict[str, Any] = Field(default_factory=dict)
    message: str = ""


class ApexTestRunner(Protocol):
    async def run_tests(
        self, *, source_dir: Path, jira_key: str, class_names: Optional[List[str]] = None
    ) -> ApexTestResult: ...


class MockApexTestRunner:
    """No-op Apex test runner for local/dev and CI."""

    def __init__(self) -> None:
        self.calls: List[Dict[str, object]] = []

    async def run_tests(
        self, *, source_dir: Path, jira_key: str, class_names: Optional[List[str]] = None
    ) -> ApexTestResult:
        self.calls.append(
            {"source_dir": str(source_dir), "jira_key": jira_key, "class_names": class_names or []}
        )
        return ApexTestResult(
            success=True,
            tests_run=0,
            tests_passed=0,
            failures=[],
            message="Mock Apex tests skipped (no test classes in stubs)",
            details={"provider": "mock"},
        )


class CliApexTestRunner:
    """Run `sf apex run test` against a lower-org alias."""

    def __init__(self, settings: Optional[Settings] = None) -> None:
        self.settings = settings or get_settings()

    async def run_tests(
        self, *, source_dir: Path, jira_key: str, class_names: Optional[List[str]] = None
    ) -> ApexTestResult:
        alias = (self.settings.salesforce_org_alias or "").strip()
        if not alias:
            raise ValueError("SALESFORCE_ORG_ALIAS is required when APEX_TEST_PROVIDER=cli")
        if _looks_like_production(alias):
            raise ValueError(
                f"Refusing Apex test run on production-like alias '{alias}' "
                "(v1 allows lower orgs only)"
            )
        if not class_names:
            return ApexTestResult(
                success=True,
                tests_run=0,
                tests_passed=0,
                failures=[],
                message="No Apex test classes to run",
                details={"provider": "cli", "skipped": True},
            )

        cmd = [
            "sf",
            "apex",
            "run",
            "test",
            "--tests",
            ",".join(class_names),
            "--target-org",
            alias,
            "--result-format",
            "json",
            "--synchronous",
            "--json",
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout_b, stderr_b = await proc.communicate()
        stdout = (stdout_b or b"").decode("utf-8", errors="replace")
        stderr = (stderr_b or b"").decode("utf-8", errors="replace")

        payload: Dict[str, Any] = {}
        try:
            payload = json.loads(stdout) if stdout.strip() else {}
        except json.JSONDecodeError:
            payload = {"raw_stdout": stdout}

        result_block = payload.get("result") if isinstance(payload.get("result"), dict) else {}
        summary = result_block.get("summary") if isinstance(result_block.get("summary"), dict) else {}
        tests_run = int(summary.get("testsRan") or summary.get("testsRun") or 0)
        tests_passed = int(summary.get("passing") or summary.get("passed") or 0)
        failures_raw = result_block.get("failures") or result_block.get("tests") or []
        failures: List[str] = []
        if isinstance(failures_raw, list):
            for item in failures_raw:
                if isinstance(item, dict):
                    failures.append(
                        f"{item.get('name', 'unknown')}: {item.get('message', item.get('outcome', 'failed'))}"
                    )
                else:
                    failures.append(str(item))

        success_flag = proc.returncode == 0 and not failures and tests_run == tests_passed
        if tests_run == 0 and not failures:
            success_flag = proc.returncode == 0

        return ApexTestResult(
            success=bool(success_flag),
            tests_run=tests_run,
            tests_passed=tests_passed,
            failures=failures,
            message=stderr.strip() or f"Apex tests completed for {jira_key}",
            details={
                "provider": "cli",
                "org_alias": alias,
                "returncode": proc.returncode,
                "result": result_block or payload,
            },
        )


_apex_mock_singleton: Optional[MockApexTestRunner] = None


def get_apex_test_runner(settings: Optional[Settings] = None) -> ApexTestRunner:
    global _apex_mock_singleton
    settings = settings or get_settings()
    if settings.apex_test_provider == "cli":
        return CliApexTestRunner(settings)
    if _apex_mock_singleton is None:
        _apex_mock_singleton = MockApexTestRunner()
    return _apex_mock_singleton


def reset_mock_apex_tests() -> MockApexTestRunner:
    global _apex_mock_singleton
    _apex_mock_singleton = MockApexTestRunner()
    return _apex_mock_singleton
