"""
Dynamics 365 / Power Platform integration core.

Wraps the pac CLI (Power Platform CLI) for local/dev workflows.
Every subprocess call maps 1:1 to a documented pac verb.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
import json
import os
import re
import shutil
import subprocess
import time

from g_gremlin.profile_manager import ProfileStore, ProfileNotFoundError
from g_gremlin.contracts import CapabilityCheck, DoctorResult

CONFIG_PATH = Path.home() / ".g_gremlin" / "dynamics.json"
DEFAULT_PROFILE = "default"


class DynamicsError(Exception):
    pass


class PacCliError(DynamicsError):
    def __init__(self, message: str, result: "PacRunResult" | None = None):
        super().__init__(message)
        self.result = result


@dataclass
class PacCapabilities:
    """Runtime feature probe of the installed pac CLI."""
    pac_path: str
    pac_version: str
    has_solution_pack: bool
    has_solution_unpack: bool
    has_solution_publish: bool
    has_json_flag: Dict[str, bool]
    has_auth_who: bool

    def to_dict(self) -> Dict[str, Any]:
        return {
            "pac_path": self.pac_path,
            "pac_version": self.pac_version,
            "has_solution_pack": self.has_solution_pack,
            "has_solution_unpack": self.has_solution_unpack,
            "has_solution_publish": self.has_solution_publish,
            "has_json_flag": self.has_json_flag,
            "has_auth_who": self.has_auth_who,
        }


@dataclass
class PacRunResult:
    """Result of a single pac subprocess call."""
    command: List[str]
    returncode: int
    stdout: str
    stderr: str
    duration_ms: float

    def to_dict(self) -> Dict[str, Any]:
        return {
            "command": self.command,
            "returncode": self.returncode,
            "stdout": self.stdout,
            "stderr": self.stderr,
            "duration_ms": self.duration_ms,
        }


@dataclass
class PacAuthProfile:
    """Parsed pac auth profile from 'pac auth list' output."""
    index: int
    active: bool
    kind: str
    name: str
    user: str
    cloud: str
    environment_url: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "index": self.index,
            "active": self.active,
            "kind": self.kind,
            "name": self.name,
            "user": self.user,
            "cloud": self.cloud,
            "environment_url": self.environment_url,
        }


class PacWrapper:
    """
    Thin wrapper around pac CLI. Every method maps to a single pac verb.
    Captures stdout/stderr for artifact logging.
    """

    def __init__(
        self,
        pac_path: str | None = None,
        environment: str | None = None,
    ) -> None:
        resolved = pac_path or self.find()
        if not resolved:
            raise PacCliError("pac CLI not found. Install Power Platform CLI (pac).")
        self.pac_path = resolved
        self.environment = environment
        self._capabilities_cache: PacCapabilities | None = None

    def run(
        self,
        args: List[str],
        *,
        timeout: int = 120,
        env_override: str | None = None,
    ) -> PacRunResult:
        """Run pac with args, capturing stdout/stderr. Raises PacCliError on failure."""
        result = self._run_raw(args, timeout=timeout, env_override=env_override)
        if result.returncode != 0:
            raise PacCliError(f"pac {' '.join(args)} failed", result)
        return result

    def _run_raw(
        self,
        args: List[str],
        *,
        timeout: int = 120,
        env_override: str | None = None,
    ) -> PacRunResult:
        """Run pac with args, capturing stdout/stderr without enforcing zero exit code."""
        if env_override and "--environment" not in args:
            args = [*args, "--environment", env_override]
        cmd = [self.pac_path, *args]
        start = time.monotonic()
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        duration_ms = (time.monotonic() - start) * 1000.0
        return PacRunResult(
            command=cmd,
            returncode=proc.returncode,
            stdout=proc.stdout or "",
            stderr=proc.stderr or "",
            duration_ms=duration_ms,
        )

    def _env_args(self, override: str | None = None) -> List[str]:
        value = override or self.environment
        if not value:
            return []
        return ["--environment", value]

    # ---------- Auth verbs ----------
    def auth_list(self) -> List[PacAuthProfile]:
        result = self.run(["auth", "list"])
        return _parse_auth_list_output(result.stdout)

    def auth_select(self, index: int) -> None:
        self.run(["auth", "select", "--index", str(index)])

    def auth_who(self) -> Dict[str, Any]:
        result = self.run(["auth", "who"])
        return _parse_kv_or_json(result.stdout)

    # ---------- Solution verbs ----------
    def solution_list(self) -> List[Dict[str, Any]]:
        result = self.run(["solution", "list", *self._env_args()])
        return _parse_solution_list_output(result.stdout)

    def solution_export(
        self,
        name: str,
        path: Path,
        *,
        managed: bool = False,
    ) -> PacRunResult:
        args = ["solution", "export", "--name", name, "--path", str(path)]
        if managed:
            args.append("--managed")
        args += self._env_args()
        return self.run(args, timeout=600)

    def solution_import(
        self,
        path: Path,
        *,
        force_overwrite: bool = False,
        publish_changes: bool = False,
        async_: bool = True,
    ) -> PacRunResult:
        args = ["solution", "import", "--path", str(path)]
        if force_overwrite:
            args.append("--force-overwrite")
        if publish_changes:
            args.append("--publish-changes")
        if async_:
            args.append("--async")
        args += self._env_args()
        return self.run(args, timeout=900)

    def solution_pack(
        self,
        folder: Path,
        zipfile: Path,
        *,
        package_type: str = "Both",
    ) -> PacRunResult:
        args = [
            "solution",
            "pack",
            "--zipfile",
            str(zipfile),
            "--folder",
            str(folder),
        ]
        if package_type:
            args += ["--packagetype", package_type]
        return self.run(args, timeout=600)

    def solution_unpack(
        self,
        zipfile: Path,
        folder: Path,
        *,
        allow_write: bool = True,
        allow_delete: bool = False,
    ) -> PacRunResult:
        args = [
            "solution",
            "unpack",
            "--zipfile",
            str(zipfile),
            "--folder",
            str(folder),
            "--allowWrite",
            "Yes" if allow_write else "No",
            "--allowDelete",
            "Yes" if allow_delete else "No",
        ]
        return self.run(args, timeout=600)

    def solution_publish(self) -> PacRunResult:
        args = ["solution", "publish", *self._env_args()]
        return self.run(args, timeout=900)

    # ---------- Org verbs ----------
    def org_who(self) -> Dict[str, Any]:
        result = self.run(["org", "who", *self._env_args()])
        return _parse_kv_or_json(result.stdout)

    # ---------- Feature probe ----------
    def capabilities(self) -> PacCapabilities:
        """
        Probe pac installation at runtime.
        Checks: pac version, presence of solution pack/unpack/publish,
        whether --json exists for specific commands.
        Stored in doctor output for debugging.
        """
        if self._capabilities_cache:
            return self._capabilities_cache

        pac_version = ""
        help_probe = self._run_raw(["help"])
        help_text = f"{help_probe.stdout}\n{help_probe.stderr}"
        version_match = re.search(r"Version:\s*([^\r\n]+)", help_text, re.IGNORECASE)
        if version_match:
            pac_version = version_match.group(1).strip()

        solution_probe = self._run_raw(["solution", "--help"])
        auth_probe = self._run_raw(["auth", "--help"])
        solution_help = f"{solution_probe.stdout}\n{solution_probe.stderr}"
        auth_help = f"{auth_probe.stdout}\n{auth_probe.stderr}"

        has_solution_pack = bool(re.search(r"\bpack\b", solution_help))
        has_solution_unpack = bool(re.search(r"\bunpack\b", solution_help))
        has_solution_publish = bool(re.search(r"\bpublish\b", solution_help))
        has_auth_who = bool(re.search(r"\bwho\b", auth_help))

        json_flags = {
            "auth_list": _help_has_json(self, ["auth", "list", "--help"]),
            "auth_who": _help_has_json(self, ["auth", "who", "--help"]),
            "solution_list": _help_has_json(self, ["solution", "list", "--help"]),
            "solution_export": _help_has_json(self, ["solution", "export", "--help"]),
            "solution_import": _help_has_json(self, ["solution", "import", "--help"]),
            "solution_pack": _help_has_json(self, ["solution", "pack", "--help"]),
            "solution_unpack": _help_has_json(self, ["solution", "unpack", "--help"]),
            "org_who": _help_has_json(self, ["org", "who", "--help"]),
        }

        self._capabilities_cache = PacCapabilities(
            pac_path=self.pac_path,
            pac_version=pac_version,
            has_solution_pack=has_solution_pack,
            has_solution_unpack=has_solution_unpack,
            has_solution_publish=has_solution_publish,
            has_json_flag=json_flags,
            has_auth_who=has_auth_who,
        )
        return self._capabilities_cache

    @staticmethod
    def find() -> str | None:
        """Find pac CLI. Checks PATH, then local user install locations, then Program Files."""
        found = shutil.which("pac")
        if found:
            return found

        local_appdata = str(os.getenv("LOCALAPPDATA") or "").strip()
        if local_appdata:
            base = Path(local_appdata) / "Microsoft" / "PowerAppsCLI"
            if base.exists():
                installs = sorted(
                    base.glob("Microsoft.PowerApps.CLI.*"),
                    key=_pac_install_version_key,
                    reverse=True,
                )
                for install_dir in installs:
                    candidate = install_dir / "tools" / "pac.exe"
                    if candidate.exists():
                        return str(candidate)

        candidates = [
            r"C:\Program Files\PowerApps CLI\pac.exe",
            r"C:\Program Files (x86)\PowerApps CLI\pac.exe",
            r"C:\Program Files\Microsoft Power Platform CLI\pac.exe",
            r"C:\Program Files (x86)\Microsoft Power Platform CLI\pac.exe",
        ]
        for path in candidates:
            if Path(path).exists():
                return path
        return None


@dataclass
class DynamicsConfig:
    """g-gremlin profile metadata for a Dynamics 365 environment."""
    profile: str = DEFAULT_PROFILE
    org_url: str = ""
    org_id: str = ""
    environment_id: str = ""
    friendly_name: str = ""
    pac_profile_index: Optional[int] = None
    pac_profile_name: Optional[str] = None
    verified: bool = False
    verified_at: Optional[str] = None
    user_email: str = ""
    tenant_id: str = ""

    @classmethod
    def load(cls, profile: str | None = None) -> "DynamicsConfig":
        store = ProfileStore.load(CONFIG_PATH, default_profile=DEFAULT_PROFILE)
        profile_name = profile or store.current_profile_name()
        if not CONFIG_PATH.exists():
            return cls(profile=profile_name)
        try:
            data = store.get_profile(profile_name)
        except ProfileNotFoundError:
            return cls(profile=profile_name)
        return cls(
            profile=profile_name,
            org_url=data.get("org_url", ""),
            org_id=data.get("org_id", ""),
            environment_id=data.get("environment_id", ""),
            friendly_name=data.get("friendly_name", ""),
            pac_profile_index=data.get("pac_profile_index"),
            pac_profile_name=data.get("pac_profile_name"),
            verified=bool(data.get("verified", False)),
            verified_at=data.get("verified_at"),
            user_email=data.get("user_email", ""),
            tenant_id=data.get("tenant_id", ""),
        )

    def save(self, *, set_current: bool = True) -> None:
        store = ProfileStore.load(CONFIG_PATH, default_profile=DEFAULT_PROFILE)
        data = {
            "org_url": self.org_url,
            "org_id": self.org_id,
            "environment_id": self.environment_id,
            "friendly_name": self.friendly_name,
            "pac_profile_index": self.pac_profile_index,
            "pac_profile_name": self.pac_profile_name,
            "verified": self.verified,
            "verified_at": self.verified_at,
            "user_email": self.user_email,
            "tenant_id": self.tenant_id,
        }
        store.set_profile(self.profile or DEFAULT_PROFILE, data, set_current=set_current)
        store.save()


def run_dynamics_doctor(
    pac_path: str | None = None,
    environment: str | None = None,
) -> DoctorResult:
    """
    Run diagnostics. Checks:
    1. pac CLI installed + version
    2. PacCapabilities feature probe (solution pack/unpack, --json, auth who)
    3. Current pac auth status (pac auth list + pac auth who)
    4. Environment accessibility (pac solution list as connectivity probe)

    Returns DoctorResult with capabilities in identity dict.
    """
    if not pac_path:
        pac_path = PacWrapper.find()

    checks: List[CapabilityCheck] = []
    identity: Dict[str, Any] = {}

    if not pac_path:
        checks.append(CapabilityCheck(name="pac_cli", status="error", detail="pac CLI not found"))
        return DoctorResult(provider="dynamics", checks=checks, identity=identity, overall_status="fail")

    try:
        pac = PacWrapper(pac_path=pac_path, environment=environment)
    except PacCliError as exc:
        checks.append(CapabilityCheck(name="pac_cli", status="error", detail=str(exc)))
        return DoctorResult(provider="dynamics", checks=checks, identity=identity, overall_status="fail")

    capabilities = pac.capabilities()
    checks.append(CapabilityCheck(name="pac_cli", status="ok", detail=capabilities.pac_version or "unknown"))

    auth_profiles: List[PacAuthProfile] = []
    try:
        auth_profiles = pac.auth_list()
        checks.append(CapabilityCheck(name="auth_list", status="ok", detail=str(len(auth_profiles))))
    except Exception as exc:
        checks.append(CapabilityCheck(name="auth_list", status="error", detail=str(exc)))

    auth_identity: Dict[str, Any] = {}
    auth_ok = True
    try:
        auth_identity = pac.auth_who()
        checks.append(CapabilityCheck(name="auth_who", status="ok"))
    except Exception as exc:
        auth_identity = {"error": str(exc)}
        auth_ok = False
        checks.append(CapabilityCheck(name="auth_who", status="error", detail=str(exc)))

    try:
        solutions = pac.solution_list()
        checks.append(CapabilityCheck(name="solution_list", status="ok", detail=str(len(solutions))))
    except Exception as exc:
        checks.append(CapabilityCheck(name="solution_list", status="error", detail=str(exc)))

    active_profile = next((p for p in auth_profiles if p.active), None)
    identity = {
        "pac_path": capabilities.pac_path,
        "pac_version": capabilities.pac_version,
        "capabilities": capabilities.to_dict(),
        "auth": auth_identity,
        "active_profile": active_profile.to_dict() if active_profile else None,
    }

    overall_status = "ok"
    if not auth_ok:
        overall_status = "fail"
    else:
        statuses = [c.status for c in checks]
        if "error" in statuses:
            overall_status = "fail"

    return DoctorResult(
        provider="dynamics",
        checks=checks,
        identity=identity,
        overall_status=overall_status,
    )


def _help_has_json(pac: PacWrapper, args: List[str]) -> bool:
    try:
        result = pac._run_raw(args)
        combined = f"{result.stdout}\n{result.stderr}".lower()
        return "--json" in combined
    except Exception:
        return False


def _pac_install_version_key(path: Path) -> tuple[int, ...]:
    """
    Parse local PAC install directory name into a sortable numeric version key.
    Example: Microsoft.PowerApps.CLI.2.2.1 -> (2, 2, 1)
    """
    match = re.match(r"Microsoft\.PowerApps\.CLI\.(.+)$", path.name, re.IGNORECASE)
    if not match:
        return (0,)
    version_tail = match.group(1)
    numbers = [int(part) for part in re.findall(r"\d+", version_tail)]
    return tuple(numbers) if numbers else (0,)


def _parse_kv_or_json(output: str) -> Dict[str, Any]:
    text = output.strip()
    if not text:
        return {}
    if text.startswith("{") or text.startswith("["):
        try:
            data = json.loads(text)
        except Exception:
            data = {}
        if isinstance(data, dict):
            return data
        return {"data": data}
    payload: Dict[str, Any] = {}
    for line in text.splitlines():
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        key = key.strip().lower().replace(" ", "_").replace("-", "_")
        payload[key] = value.strip()
    return payload


def _parse_auth_list_output(output: str) -> List[PacAuthProfile]:
    text = output.strip()
    if not text:
        return []
    profiles: List[PacAuthProfile] = []
    if text.startswith("{") or text.startswith("["):
        try:
            data = json.loads(text)
        except Exception:
            data = []
        if isinstance(data, dict):
            data = data.get("profiles") or data.get("Profiles") or []
        if isinstance(data, list):
            for item in data:
                if not isinstance(item, dict):
                    continue
                profiles.append(
                    PacAuthProfile(
                        index=int(item.get("index") or item.get("Index") or 0),
                        active=bool(item.get("active") or item.get("Active") or False),
                        kind=str(item.get("kind") or item.get("Kind") or ""),
                        name=str(item.get("name") or item.get("Name") or ""),
                        user=str(item.get("user") or item.get("User") or ""),
                        cloud=str(item.get("cloud") or item.get("Cloud") or ""),
                        environment_url=item.get("environment_url") or item.get("Environment"),
                    )
                )
        return profiles

    lines = [line for line in text.splitlines() if line.strip()]
    for line in lines:
        lower = line.lower()
        if lower.startswith("index") or lower.startswith("idx") or ("active" in lower and "kind" in lower):
            continue
        if set(line.strip()) == {"-"}:
            continue
        tokens = line.split()
        if not tokens:
            continue
        first = tokens[0].strip()
        index_match = re.match(r"^\[(\d+)\]$", first)
        if index_match:
            index = int(index_match.group(1))
        elif first.isdigit():
            index = int(first)
        else:
            continue
        active = False
        pos = 1
        if pos < len(tokens) and tokens[pos] in {"*", "True", "true", "Yes", "yes"}:
            active = tokens[pos] in {"*", "True", "true", "Yes", "yes"}
            pos += 1
        kind = tokens[pos] if pos < len(tokens) else ""
        pos += 1
        remaining = tokens[pos:]
        environment_url = None
        if remaining and remaining[-1].startswith("http"):
            environment_url = remaining[-1]
            remaining = remaining[:-1]
        user = ""
        cloud = ""
        name_tokens: List[str] = []
        if remaining:
            user_idx = None
            guid_pattern = re.compile(r"^[0-9a-fA-F-]{32,36}$")
            for idx, token in enumerate(remaining):
                if "@" in token or guid_pattern.match(token):
                    user_idx = idx
                    break

            if user_idx is not None:
                user = remaining[user_idx]
                name_tokens = remaining[:user_idx]
                for idx in range(user_idx + 1, len(remaining)):
                    if remaining[idx].lower() in {"public", "usgov", "china", "germany", "dod"}:
                        cloud = remaining[idx]
                        break
            else:
                cloud = remaining[-1] if remaining else ""
                reduced = remaining[:-1] if remaining else []
                user_idx = None
                for idx in range(len(reduced) - 1, -1, -1):
                    if "@" in reduced[idx]:
                        user_idx = idx
                        break
                if user_idx is None and reduced:
                    user_idx = len(reduced) - 1
                if user_idx is not None:
                    user = reduced[user_idx]
                    name_tokens = reduced[:user_idx]
        name = " ".join(name_tokens).strip()
        profiles.append(
            PacAuthProfile(
                index=index,
                active=active,
                kind=kind,
                name=name,
                user=user,
                cloud=cloud,
                environment_url=environment_url,
            )
        )
    return profiles


def _parse_solution_list_output(output: str) -> List[Dict[str, Any]]:
    text = output.strip()
    if not text:
        return []
    if text.startswith("{") or text.startswith("["):
        try:
            data = json.loads(text)
        except Exception:
            data = []
        if isinstance(data, dict):
            data = data.get("solutions") or data.get("Solutions") or []
        if isinstance(data, list):
            return [_normalize_solution_entry(item) for item in data if isinstance(item, dict)]
        return []

    lines = [line for line in text.splitlines() if line.strip()]
    solutions: List[Dict[str, Any]] = []
    for line in lines:
        lower = line.lower()
        if "uniquename" in lower or "unique name" in lower:
            continue
        if set(line.strip()) == {"-"}:
            continue
        tokens = line.split()
        if len(tokens) < 2:
            continue
        version_idx = None
        for idx in range(len(tokens) - 1, -1, -1):
            if re.match(r"^\d+(\.\d+)+$", tokens[idx]):
                version_idx = idx
                break
        if version_idx is None:
            continue
        version = tokens[version_idx]
        managed = None
        if version_idx + 1 < len(tokens):
            managed_token = tokens[version_idx + 1].lower()
            if managed_token in {"true", "false", "managed", "unmanaged", "yes", "no"}:
                managed = managed_token in {"true", "managed", "yes"}
        unique_name = tokens[0]
        friendly_name = " ".join(tokens[1:version_idx]).strip()
        solutions.append(
            {
                "unique_name": unique_name,
                "friendly_name": friendly_name,
                "version": version,
                "managed": managed,
            }
        )
    return solutions


def _normalize_solution_entry(item: Dict[str, Any]) -> Dict[str, Any]:
    return {
        "unique_name": item.get("unique_name")
        or item.get("UniqueName")
        or item.get("SolutionUniqueName")
        or item.get("name")
        or item.get("Name"),
        "friendly_name": item.get("friendly_name")
        or item.get("FriendlyName")
        or item.get("DisplayName")
        or item.get("displayName")
        or item.get("name")
        or item.get("Name"),
        "version": item.get("version") or item.get("Version"),
        "managed": item.get("managed") or item.get("Managed"),
        "raw": item,
    }
