"""
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 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."""
        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
        result = PacRunResult(
            command=cmd,
            returncode=proc.returncode,
            stdout=proc.stdout or "",
            stderr=proc.stderr or "",
            duration_ms=duration_ms,
        )
        if proc.returncode != 0:
            raise PacCliError(f"pac {' '.join(args)} failed", result)
        return result

    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 = ""
        try:
            version_out = self.run(["--version"]).stdout.strip()
            pac_version = version_out.splitlines()[-1].strip()
        except Exception:
            pac_version = ""

        solution_help = ""
        auth_help = ""
        try:
            solution_help = self.run(["solution", "--help"]).stdout
        except Exception:
            solution_help = ""
        try:
            auth_help = self.run(["auth", "--help"]).stdout
        except Exception:
            auth_help = ""

        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 Windows Program Files."""
        found = shutil.which("pac")
        if found:
            return found
        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:
        out = pac.run(args).stdout
        return "--json" in out
    except Exception:
        return False


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
        if not tokens[0].isdigit():
            continue
        index = int(tokens[0])
        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]
        cloud = remaining[-1] if remaining else ""
        remaining = remaining[:-1] if remaining else []
        user = ""
        name_tokens: List[str] = []
        if remaining:
            user_idx = None
            for idx in range(len(remaining) - 1, -1, -1):
                if "@" in remaining[idx]:
                    user_idx = idx
                    break
            if user_idx is None:
                user_idx = len(remaining) - 1
            user = remaining[user_idx]
            name_tokens = remaining[: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,
    }
