"""Duplicate package copy (for importable package name `py_azpe`).
This file mirrors the implementation in the repository folder `azure-policy-engine/engine.py`.
"""
from __future__ import annotations

import importlib
import json
import os
from typing import Any, Dict, List, Optional, TYPE_CHECKING


# Optional type-only imports for static analysis
if TYPE_CHECKING:
    try:
        from azure.mgmt.resource import PolicyClient  # type: ignore
        import azure.mgmt.resource.policy as _azure_mgmt_policy  # type: ignore
    except Exception:  # pragma: no cover - only for static analysis
        PolicyClient = Any  # type: ignore
        _azure_mgmt_policy = Any  # type: ignore


def _import_attr(mod_name: str, attr: str):
    try:
        mod = importlib.import_module(mod_name)
        return getattr(mod, attr, None)
    except Exception:
        return None


CJSON_EXT = ".json"
_AZURE_MGMT_RESOURCE = "azure.mgmt.resource"
_AZURE_MGMT_POLICY_MODELS = "azure.mgmt.resource.policy.models"
_CREDENTIAL_ERR = "Azure credential not provided for SDK/ARM calls"


class AzurePolicyEngine:
    def __init__(
        self,
        backend: str = "file",
        policy_dir: Optional[str] = None,
        subscription_id: Optional[str] = None,
        credential: Optional[Any] = None,
    ) -> None:
        self.backend = backend
        base = os.path.dirname(__file__)
        self.policy_dir = policy_dir or os.path.join(base, "policies")
        # allow defaults from environment via azure_auth helpers
        self.subscription_id = subscription_id
        self.credential = credential
        if self.subscription_id is None or self.credential is None:
            try:
                # lazy import so we don't require azure packages for file/azcli backends
                from .azure_auth import get_subscription_id, get_credential

                if self.subscription_id is None:
                    try:
                        self.subscription_id = get_subscription_id()
                    except Exception:
                        # keep None if not available
                        pass
                if self.credential is None:
                    try:
                        self.credential = get_credential()
                    except Exception:
                        # keep None if not available
                        pass
            except Exception:
                # azure_auth not present or import failed; ignore and continue
                pass

        # canonical folders
        self.definitions_dir = os.path.join(self.policy_dir, "definitions")
        self.assignments_dir = os.path.join(self.policy_dir, "assignments")
        self.initiatives_dir = os.path.join(self.policy_dir, "initiatives")

        os.makedirs(self.definitions_dir, exist_ok=True)
        os.makedirs(self.assignments_dir, exist_ok=True)
        os.makedirs(self.initiatives_dir, exist_ok=True)
        # optional azcli backend helper (lazy import)
        self._azcli = None

    # -----------------
    # File backend
    # -----------------
    def _file_list_policies(self) -> List[Dict[str, Any]]:
        results: List[Dict[str, Any]] = []
        # read JSON files from the definitions subfolder
        for fname in sorted(os.listdir(self.definitions_dir)):
            if not fname.lower().endswith(CJSON_EXT):
                continue
            path = os.path.join(self.definitions_dir, fname)
            try:
                with open(path, "r", encoding="utf-8") as f:
                    body = json.load(f)
                name = body.get("name") or os.path.splitext(fname)[0]
                results.append(
                    {
                        "name": name,
                        "file": path,
                        "properties": body.get("properties", body),
                    }
                )
            except Exception as ex:
                # include an entry with the error so callers can see problematic files
                results.append({"file": path, "error": str(ex)})
        return results

    def _file_get_policy(self, name: str) -> Dict[str, Any]:
        # try exact filename then name.json
        candidates = [name, f"{name}{CJSON_EXT}"]
        for c in candidates:
            path = os.path.join(self.definitions_dir, c)
            if os.path.exists(path):
                with open(path, "r", encoding="utf-8") as f:
                    return json.load(f)
        # fallback: search files for matching 'name' property
        for fname in os.listdir(self.definitions_dir):
            if not fname.lower().endswith(CJSON_EXT):
                continue
            path = os.path.join(self.definitions_dir, fname)
            try:
                with open(path, "r", encoding="utf-8") as f:
                    body = json.load(f)
                if body.get("name") == name or os.path.splitext(fname)[0] == name:
                    return body
            except Exception:
                continue
        raise FileNotFoundError(f"policy '{name}' not found in {self.policy_dir}")

    def _file_deploy_policy(self, name: str, policy: Dict[str, Any]) -> Dict[str, Any]:
        path = os.path.join(self.definitions_dir, f"{name}{CJSON_EXT}")
        with open(path, "w", encoding="utf-8") as f:
            json.dump(policy, f, indent=2)
        return {"file": path, "status": "written"}

    def _file_list_assignments(self) -> List[Dict[str, Any]]:
        results: List[Dict[str, Any]] = []
        for fname in sorted(os.listdir(self.assignments_dir)):
            if not fname.lower().endswith(CJSON_EXT):
                continue
            path = os.path.join(self.assignments_dir, fname)
            try:
                with open(path, "r", encoding="utf-8") as f:
                    body = json.load(f)
                name = body.get("name") or os.path.splitext(fname)[0]
                results.append(
                    {
                        "name": name,
                        "file": path,
                        "properties": body.get("properties", body),
                    }
                )
            except Exception as ex:
                results.append({"file": path, "error": str(ex)})
        return results

    def _file_list_initiatives(self) -> List[Dict[str, Any]]:
        results: List[Dict[str, Any]] = []
        for fname in sorted(os.listdir(self.initiatives_dir)):
            if not fname.lower().endswith(CJSON_EXT):
                continue
            path = os.path.join(self.initiatives_dir, fname)
            try:
                with open(path, "r", encoding="utf-8") as f:
                    body = json.load(f)
                name = body.get("name") or os.path.splitext(fname)[0]
                results.append(
                    {
                        "name": name,
                        "file": path,
                        "properties": body.get("properties", body),
                    }
                )
            except Exception as ex:
                results.append({"file": path, "error": str(ex)})
        return results

    def _file_get_initiative(self, name: str) -> Dict[str, Any]:
        candidates = [name, f"{name}{CJSON_EXT}"]
        for c in candidates:
            path = os.path.join(self.initiatives_dir, c)
            if os.path.exists(path):
                with open(path, "r", encoding="utf-8") as f:
                    return json.load(f)
        for fname in os.listdir(self.initiatives_dir):
            if not fname.lower().endswith(CJSON_EXT):
                continue
            path = os.path.join(self.initiatives_dir, fname)
            try:
                with open(path, "r", encoding="utf-8") as f:
                    body = json.load(f)
                if body.get("name") == name or os.path.splitext(fname)[0] == name:
                    return body
            except Exception:
                continue
        raise FileNotFoundError(
            f"initiative '{name}' not found in {self.initiatives_dir}"
        )

    def _file_deploy_initiative(
        self, name: str, initiative: Dict[str, Any]
    ) -> Dict[str, Any]:
        path = os.path.join(self.initiatives_dir, f"{name}{CJSON_EXT}")
        with open(path, "w", encoding="utf-8") as f:
            json.dump(initiative, f, indent=2)
        return {"file": path, "status": "written"}

    # -----------------
    # Public API
    # -----------------
    def list_policies(self) -> List[Dict[str, Any]]:
        """List available policy definitions from the selected backend.

        Returns a list of dicts with at least 'name' and 'properties'.
        """
        if self.backend == "file":
            return self._file_list_policies()
        elif self.backend == "sdk":
            return self._sdk_list_policies()
        elif self.backend == "azcli":
            # lazy import to avoid requiring azcli module at package import time
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            vals = self._azcli.list_policy_definitions()
            # normalize to expected list items with name/properties
            out = []
            for v in vals:
                name = v.get("name") or v.get("properties", {}).get("displayName")
                out.append(
                    {"name": name, "file": None, "properties": v.get("properties", v)}
                )
            return out
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def get_policy(self, name: str) -> Dict[str, Any]:
        if self.backend == "file":
            return self._file_get_policy(name)
        elif self.backend == "sdk":
            return self._sdk_get_policy(name)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.get_policy_definition(name)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def deploy_policy(self, name: str, policy: Dict[str, Any]) -> Dict[str, Any]:
        """Create or update a policy definition with the given name and policy body."""
        if self.backend == "file":
            return self._file_deploy_policy(name, policy)
        elif self.backend == "sdk":
            return self._sdk_deploy_policy(name, policy)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.create_or_update_policy_definition(name, policy)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def list_assignments(self, scope: Optional[str] = None) -> List[Dict[str, Any]]:
        if self.backend == "file":
            return self._file_list_assignments()
        elif self.backend == "sdk":
            return self._sdk_list_assignments(scope)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            vals = self._azcli.list_assignments(scope)
            out = []
            for v in vals:
                out.append(
                    {
                        "name": v.get("name"),
                        "file": None,
                        "properties": v.get("properties", v),
                    }
                )
            return out
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def list_initiatives(self) -> List[Dict[str, Any]]:
        """List initiatives (policy set definitions)."""
        if self.backend == "file":
            return self._file_list_initiatives()
        elif self.backend == "sdk":
            return self._sdk_list_initiatives()
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            vals = self._azcli.list_initiatives()
            out = []
            for v in vals:
                out.append(
                    {
                        "name": v.get("name"),
                        "file": None,
                        "properties": v.get("properties", v),
                    }
                )
            return out
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def get_initiative(self, name: str) -> Dict[str, Any]:
        if self.backend == "file":
            return self._file_get_initiative(name)
        elif self.backend == "sdk":
            return self._sdk_get_initiative(name)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.get_initiative(name)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def deploy_initiative(
        self, name: str, initiative: Dict[str, Any]
    ) -> Dict[str, Any]:
        if self.backend == "file":
            return self._file_deploy_initiative(name, initiative)
        elif self.backend == "sdk":
            return self._sdk_deploy_initiative(name, initiative)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.create_or_update_initiative(name, initiative)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    # -----------------
    # Scope validation and dry-run helpers
    # -----------------
    def _validate_scope(self, scope: str) -> str:
        """Quick heuristic validation for allowed Azure policy assignment scopes.

        Accepts:
        - subscription: /subscriptions/{subscriptionId}
        - resource group: /subscriptions/{subscriptionId}/resourceGroups/{rg}
        - management group: /providers/Microsoft.Management/managementGroups/{mg}
        - tenant or other well-formed Azure resource id starting with a '/'

        Returns the normalized scope (trimmed) or raises ValueError on clearly invalid values.
        """
        if not scope or not isinstance(scope, str):
            raise ValueError("scope must be a non-empty string")
        s = scope.strip()
        # must start with '/'
        if not s.startswith("/"):
            raise ValueError(
                "scope must be a resource id starting with '/' (e.g. /subscriptions/..., /providers/Microsoft.Management/managementGroups/...)"
            )

        # simple patterns
        lower = s.lower()
        if lower.startswith("/subscriptions/"):
            # either subscription root or resource group
            # valid forms: /subscriptions/{id} or /subscriptions/{id}/resourceGroups/{rg}
            parts = s.split("/")
            if len(parts) >= 3 and parts[2]:
                return s
            raise ValueError("invalid subscription scope")
        if lower.startswith("/providers/microsoft.management/managementgroups/"):
            parts = s.split("/")
            # expect at least 5 parts: '', 'providers','Microsoft.Management','managementGroups','{id}'
            if len(parts) >= 5 and parts[4]:
                return s
            raise ValueError("invalid management group scope")

        # fallback: accept any starting with '/'
        return s

    def prepare_assignment_payload(
        self, name: str, assignment: Dict[str, Any], scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Return the final payload and effective scope that would be used to create/update an assignment.

        Useful for dry-run: returns a dict with keys: scope, name, payload (dict or SDK model converted to dict when possible).
        """
        if scope:
            scope = self._validate_scope(scope)
        # build payload: prefer SDK model when available
        payload = assignment.copy()
        # try to construct SDK model for better representation (but if not available, keep dict)
        try:
            model = self._make_policy_assignment_model(payload)
            as_dict = getattr(model, "as_dict", None)
            if as_dict:
                payload_repr = as_dict()
            elif isinstance(model, dict):
                payload_repr = model
            else:
                payload_repr = payload
        except Exception:
            payload_repr = payload
        return {"scope": scope, "name": name, "payload": payload_repr}

    def _determine_scope_path(self, scope: Optional[str]) -> str:
        """Return the normalized scope path used for ARM REST URLs.

        If scope is provided, return it trimmed; otherwise return subscription-level path if available or empty string.
        """
        if scope:
            return scope.rstrip("/")
        if self.subscription_id:
            return f"/subscriptions/{self.subscription_id}"
        return ""

    # -----------------
    # Assignment public operations
    # -----------------
    def create_assignment(
        self, name: str, assignment: Dict[str, Any], scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Create an assignment for a policy definition. In file backend it writes into assignments/; in sdk backend it creates the assignment resource."""
        if scope:
            scope = self._validate_scope(scope)
        if self.backend == "file":
            return self._file_deploy_assignment(name, assignment)
        elif self.backend == "sdk":
            return self._sdk_create_assignment(name, assignment, scope)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.create_or_update_assignment(name, assignment, scope)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def update_assignment(
        self, name: str, assignment: Dict[str, Any], scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Update an existing assignment (or create if it doesn't exist)."""
        if scope:
            scope = self._validate_scope(scope)
        # update is equivalent to create in our model
        return self.create_assignment(name, assignment, scope)

    def delete_assignment(
        self, name: str, scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Delete an assignment by name (and scope)."""
        if scope:
            scope = self._validate_scope(scope)
        if self.backend == "file":
            path = os.path.join(self.assignments_dir, f"{name}{CJSON_EXT}")
            if os.path.exists(path):
                os.remove(path)
                return {"file": path, "status": "deleted"}
            return {"file": path, "status": "not_found"}
        elif self.backend == "sdk":
            return self._sdk_delete_assignment(name, scope)
        elif self.backend == "azcli":
            if self._azcli is None:
                try:
                    from .azcli_backend import AzCliBackend  # type: ignore

                    self._azcli = AzCliBackend(subscription_id=self.subscription_id)
                except Exception:
                    raise RuntimeError(
                        "azcli backend is not available (ensure 'az' is installed)"
                    )
            return self._azcli.delete_assignment(name, scope)
        else:
            raise ValueError(f"unknown backend: {self.backend}")

    def assign_initiative(
        self,
        _initiative_name: str,
        assignment_name: str,
        assignment: Dict[str, Any],
        scope: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Create an assignment for an initiative (policy set). In file backend it writes into assignments/; in sdk backend it creates the assignment resource."""
        if scope:
            scope = self._validate_scope(scope)
        # assignment should reference initiative id in properties; we just create assignment resource
        return self.create_assignment(assignment_name, assignment, scope)

    # -----------------
    # SDK helpers (use azure-mgmt if present, otherwise ARM REST fallbacks)
    # -----------------
    def _make_policy_assignment_model(self, assignment: Dict[str, Any]):
        """Try to build an SDK PolicyAssignment model. Fall back to the dict if not available."""
        # dynamic import to avoid static analyzer warnings
        sdk_policy_assignment = _import_attr(
            _AZURE_MGMT_POLICY_MODELS, "PolicyAssignment"
        )
        if sdk_policy_assignment is None:
            # fallback to alternate location
            policy_mod = _import_attr(_AZURE_MGMT_RESOURCE, "policy")
            if policy_mod:
                sdk_policy_assignment = getattr(
                    getattr(policy_mod, "models", {}), "PolicyAssignment", None
                )

        if sdk_policy_assignment:
            try:
                return sdk_policy_assignment(**assignment)
            except Exception:
                # model construction failed; return raw dict
                return assignment
        return assignment

    def _sdk_create_assignment(
        self, name: str, assignment: Dict[str, Any], scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Create an assignment using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            params = assignment.copy()
            model = self._make_policy_assignment_model(params)
            res = client.policy_assignments.create(scope, name, model)
            as_dict = getattr(res, "as_dict", None)
            return as_dict() if as_dict else getattr(res, "__dict__", {})
        # ARM REST fallback
        if not self.credential:
            raise RuntimeError(_CREDENTIAL_ERR)
        api = "2021-06-01"
        scope_path = self._determine_scope_path(scope)
        url = f"https://management.azure.com{scope_path}/providers/Microsoft.Authorization/policyAssignments/{name}?api-version={api}"
        return self._arm_request("PUT", url, json=assignment.copy())

    def _sdk_delete_assignment(
        self, name: str, scope: Optional[str] = None
    ) -> Dict[str, Any]:
        """Delete an assignment using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            res = client.policy_assignments.delete(scope, name)
            return {"status": "deleted", "raw": getattr(res, "as_dict", lambda: {})()}
        if not self.credential:
            raise RuntimeError(_CREDENTIAL_ERR)
        api = "2021-06-01"
        scope_path = self._determine_scope_path(scope)
        url = f"https://management.azure.com{scope_path}/providers/Microsoft.Authorization/policyAssignments/{name}?api-version={api}"
        try:
            resp = self._arm_request("DELETE", url)
            return {"status": "deleted", "response": resp}
        except Exception as ex:
            return {"status": "error", "error": str(ex)}

    def _sdk_list_assignments(
        self, scope: Optional[str] = None
    ) -> List[Dict[str, Any]]:
        """List assignments using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            if scope:
                items = list(client.policy_assignments.list_for_scope(scope))
            else:
                try:
                    items = list(client.policy_assignments.list())
                except Exception:
                    items = list(
                        client.policy_assignments.list_for_subscription(
                            self.subscription_id
                        )
                    )
            out: List[Dict[str, Any]] = []
            for it in items:
                as_dict = getattr(it, "as_dict", None)
                out.append(as_dict() if as_dict else getattr(it, "__dict__", {}))
            return out
        # ARM REST fallback
        if not self.credential:
            raise RuntimeError(_CREDENTIAL_ERR)
        api = "2021-06-01"
        scope_path = self._determine_scope_path(scope)
        url = f"https://management.azure.com{scope_path}/providers/Microsoft.Authorization/policyAssignments?api-version={api}"
        resp = self._arm_request("GET", url)
        return resp.get("value", [])

    # -----------------
    # Minimal policy and initiative SDK helpers (used by other features/tests)
    # -----------------
    def _get_arm_token(self) -> str:
        """Acquire an Azure AD token for ARM (management) if a credential is available."""
        if not self.credential:
            raise RuntimeError(
                "Azure credential not provided. Pass an azure.identity credential to AzurePolicyEngine(credential=...) when using the sdk backend."
            )
        token = self.credential.get_token("https://management.azure.com/.default")
        return token.token

    def _arm_request(self, method: str, url: str, **kwargs) -> Dict[str, Any]:
        """Helper for ARM REST calls using requests and an acquired bearer token."""
        try:
            import requests
        except Exception:
            raise RuntimeError(
                "requests library is required for sdk backend; add 'requests' to your environment"
            )
        token = self._get_arm_token()
        headers = kwargs.pop("headers", {})
        headers.update(
            {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
        )
        r = requests.request(method, url, headers=headers, **kwargs)
        try:
            body = r.json()
        except Exception:
            body = {"raw_text": r.text}
        if not r.ok:
            raise RuntimeError(
                f"ARM request {method} {url} failed: {r.status_code} {r.reason} - {body}"
            )
        return body

    def _sdk_list_policies(self) -> List[Dict[str, Any]]:
        """List policy definitions using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            items = list(client.policy_definitions.list())
            out: List[Dict[str, Any]] = []
            for it in items:
                as_dict = getattr(it, "as_dict", None)
                out.append(as_dict() if as_dict else getattr(it, "__dict__", {}))
            return out
        if not self.credential:
            raise RuntimeError("Azure credential not provided for SDK/ARM calls")
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policyDefinitions?api-version={api}"
        resp = self._arm_request("GET", url)
        return resp.get("value", [])

    def _sdk_get_policy(self, name: str) -> Dict[str, Any]:
        """Get a single policy definition by name using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            item = client.policy_definitions.get(name)
            as_dict = getattr(item, "as_dict", None)
            return as_dict() if as_dict else getattr(item, "__dict__", {})
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policyDefinitions/{name}?api-version={api}"
        return self._arm_request("GET", url)

    def _sdk_deploy_policy(self, name: str, policy: Dict[str, Any]) -> Dict[str, Any]:
        """Create or update a policy definition using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            body = policy.copy()
            body.pop("name", None)
            model = self._make_policy_definition_model(body)
            try:
                res = client.policy_definitions.create_or_update(name, model)
            except Exception:
                res = client.policy_definitions.create(name, model)
            as_dict = getattr(res, "as_dict", None)
            return as_dict() if as_dict else getattr(res, "__dict__", {})
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policyDefinitions/{name}?api-version={api}"
        body = policy.copy()
        body.pop("name", None)
        return self._arm_request("PUT", url, json=body)

    def _make_policy_definition_model(self, policy: Dict[str, Any]):
        """Construct a policy definition model object for SDK use, or return the original policy dict."""
        sdk_def = _import_attr(_AZURE_MGMT_POLICY_MODELS, "PolicyDefinition")
        if sdk_def is None:
            policy_mod = _import_attr(_AZURE_MGMT_RESOURCE, "policy")
            if policy_mod:
                sdk_def = getattr(
                    getattr(policy_mod, "models", {}), "PolicyDefinition", None
                )
        if sdk_def:
            try:
                return sdk_def(**policy)
            except Exception:
                return policy
        return policy

    def _sdk_list_initiatives(self) -> List[Dict[str, Any]]:
        """List initiative (policy set) definitions using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            items = list(client.policy_set_definitions.list())
            out: List[Dict[str, Any]] = []
            for it in items:
                as_dict = getattr(it, "as_dict", None)
                out.append(as_dict() if as_dict else getattr(it, "__dict__", {}))
            return out
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policySetDefinitions?api-version={api}"
        resp = self._arm_request("GET", url)
        return resp.get("value", [])

    def _sdk_get_initiative(self, name: str) -> Dict[str, Any]:
        """Get a single initiative (policy set) definition by name using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            if name.startswith("/"):
                # azure-mgmt client typically needs name, so fallback to ARM
                raise RuntimeError("full resource-id get not supported by SDK path")
            item = client.policy_set_definitions.get(name)
            as_dict = getattr(item, "as_dict", None)
            return as_dict() if as_dict else getattr(item, "__dict__", {})
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        if name.startswith("/"):
            url = f"https://management.azure.com{name}?api-version={api}"
        else:
            url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policySetDefinitions/{name}?api-version={api}"
        return self._arm_request("GET", url)

    def _sdk_deploy_initiative(
        self, name: str, initiative: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Create or update an initiative (policy set) definition using the SDK (azure-mgmt) if available, otherwise fallback to ARM REST API."""
        policy_client_cls = _import_attr(_AZURE_MGMT_RESOURCE, "PolicyClient")
        if policy_client_cls:
            client = policy_client_cls(self.credential, self.subscription_id)
            body = initiative.copy()
            body.pop("name", None)
            model = self._make_policy_set_definition_model(body)
            try:
                res = client.policy_set_definitions.create_or_update(name, model)
            except Exception:
                res = client.policy_set_definitions.create(name, model)
            as_dict = getattr(res, "as_dict", None)
            return as_dict() if as_dict else getattr(res, "__dict__", {})
        api = "2021-06-01"
        scope = f"/subscriptions/{self.subscription_id}" if self.subscription_id else ""
        url = f"https://management.azure.com{scope}/providers/Microsoft.Authorization/policySetDefinitions/{name}?api-version={api}"
        body = initiative.copy()
        body.pop("name", None)
        return self._arm_request("PUT", url, json=body)

    def _make_policy_set_definition_model(self, initiative: Dict[str, Any]):
        """Construct a policy set definition model object for SDK use, or return the original initiative dict."""
        sdk_cls = _import_attr(_AZURE_MGMT_POLICY_MODELS, "PolicySetDefinition")
        if sdk_cls is None:
            policy_mod = _import_attr(_AZURE_MGMT_RESOURCE, "policy")
            if policy_mod:
                sdk_cls = getattr(
                    getattr(policy_mod, "models", {}), "PolicySetDefinition", None
                )
        if sdk_cls:
            try:
                return sdk_cls(**initiative)
            except Exception:
                return initiative
        return initiative


# end of engine.py
