"""Dynamics CRM provider for Scout checks (Dataverse Web API)."""

from __future__ import annotations

import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List

from g_gremlin.credentials.provider import CredentialProvider

from ..crm_provider import Filter, FilterExpr, FilterOp, OrGroup
from ..field_mapping import FieldMapper
from .dynamics_auth import DataverseAuthError, DataverseAuthenticator
from .dynamics_client import DataverseClient


class DynamicsProvider:
    name = "dynamics"
    supports_writes = False

    def __init__(
        self,
        *,
        environment_url: str | None = None,
        tenant_id: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        profile: str | None = None,
        field_overrides: Dict[str, Dict[str, str]] | None = None,
        credential_provider: CredentialProvider | None = None,
    ) -> None:
        self.profile = profile or "default"
        self.credential_provider = credential_provider or CredentialProvider()
        self.mapper = FieldMapper("dynamics", overrides=field_overrides)
        self._warnings: List[str] = []
        self._queries: List[str] = []
        self._metadata_cache: Dict[str, Dict[str, Any]] = {}

        self.environment_url = self._resolve_value(
            "environment_url",
            environment_url,
            "G_GREMLIN_DYNAMICS_ENVIRONMENT_URL",
        )
        if not self.environment_url:
            raise DataverseAuthError("Missing Dataverse credentials: environment_url")

        self.tenant_id = self._resolve_value(
            "tenant_id",
            tenant_id,
            "G_GREMLIN_DYNAMICS_TENANT_ID",
        )
        self.client_id = self._resolve_value(
            "client_id",
            client_id,
            "G_GREMLIN_DYNAMICS_CLIENT_ID",
        )
        self.client_secret = self._resolve_value(
            "client_secret",
            client_secret,
            "G_GREMLIN_DYNAMICS_CLIENT_SECRET",
        )

        self.authenticator = DataverseAuthenticator(
            tenant_id=self.tenant_id,
            client_id=self.client_id,
            client_secret=self.client_secret,
            environment_url=self.environment_url,
        )
        self.client = DataverseClient(
            self.environment_url,
            self.authenticator,
            credential_provider=self.credential_provider,
        )

    def count(self, object_type: str, filters: List[FilterExpr]) -> int:
        native_object = self.mapper.native_object(object_type)
        if not native_object:
            return 0
        filter_expr = self._build_filter(object_type, filters)
        payload = self.client.query(
            native_object,
            filter_expr=filter_expr or None,
            top=0,
            count=True,
        )
        self._queries.append(
            self._format_query(native_object, filter_expr, top=0, count=True)
        )
        count_value = self._parse_count(payload.get("@odata.count"))
        capped = payload.get(
            "@Microsoft.Dynamics.CRM.totalrecordcountlimitexceeded",
            False,
        )
        if capped:
            self._warnings.append(
                f"Dataverse count capped at {count_value}; actual total may be higher"
            )
        return count_value

    def query(
        self,
        object_type: str,
        fields: List[str],
        filters: List[FilterExpr],
        order_by: str | None = None,
        order_dir: str = "ASC",
        nulls_first: bool = False,
        limit: int = 100,
    ) -> List[Dict[str, Any]]:
        native_object = self.mapper.native_object(object_type)
        if not native_object:
            return []
        native_fields = self.mapper.to_native_fields(object_type, fields)
        pk_field = f"{native_object}id"
        if pk_field not in native_fields:
            native_fields.append(pk_field)

        filter_expr = self._build_filter(object_type, filters)
        orderby_expr = None
        if order_by:
            native_order = self.mapper.to_native_field(object_type, order_by)
            direction = "desc" if order_dir.upper() == "DESC" else "asc"
            orderby_expr = f"{native_order} {direction}"

        top = int(limit) if limit else 100
        payload = self.client.query(
            native_object,
            select=native_fields,
            filter_expr=filter_expr or None,
            orderby=orderby_expr,
            top=top,
            count=False,
        )
        self._queries.append(
            self._format_query(
                native_object,
                filter_expr,
                top=top,
                orderby=orderby_expr,
                select=native_fields,
            )
        )
        rows = payload.get("value", []) or []
        normalized: List[Dict[str, Any]] = []
        for row in rows:
            canonical = self.mapper.from_native_row(object_type, row)
            if "Id" not in canonical:
                pk_value = row.get(pk_field)
                if pk_value is not None:
                    canonical["Id"] = pk_value
            normalized.append(canonical)
        return normalized

    def describe(self, object_type: str) -> Dict[str, Any]:
        key = str(object_type).lower()
        if key in self._metadata_cache:
            return self._metadata_cache[key]

        native_object = self.mapper.native_object(key) or key
        payload = self.client.get_entity_metadata(native_object)
        attributes = payload.get("value", []) or []
        fields: List[Dict[str, Any]] = []
        for attr in attributes:
            name = attr.get("LogicalName") or attr.get("logicalName")
            attr_type = attr.get("AttributeType") or attr.get("attributeType")
            label = self._extract_label(attr.get("DisplayName"))
            fields.append(
                {
                    "name": name,
                    "type": attr_type,
                    "label": label,
                }
            )
        result = {"object": native_object, "fields": fields}
        self._metadata_cache[key] = result
        return result

    def update_record(self, object_type: str, record_id: str, values: Dict[str, Any]) -> Dict[str, Any]:
        raise NotImplementedError("Dynamics provider is read-only in v1.")

    def create_record(self, object_type: str, values: Dict[str, Any]) -> Dict[str, Any]:
        raise NotImplementedError("Dynamics provider is read-only in v1.")

    def supports_object(self, object_type: str) -> bool:
        return self.mapper.native_object(str(object_type).lower()) is not None

    def drain_warnings(self) -> List[str]:
        warnings = list(self._warnings)
        self._warnings.clear()
        return warnings

    def drain_queries(self) -> List[str]:
        queries = list(self._queries)
        self._queries.clear()
        return queries

    def _resolve_value(self, key: str, provided: str | None, env_var: str) -> str | None:
        if provided:
            return provided
        env_val = os.getenv(env_var)
        if env_val:
            return env_val.strip()
        secret = self.credential_provider.get_secret_or_none(
            "dynamics",
            key,
            profile=self.profile,
        )
        if secret is not None:
            return secret.value
        return None

    def _build_filter(self, object_type: str, filters: List[FilterExpr]) -> str:
        clauses: List[str] = []
        for item in filters:
            if isinstance(item, OrGroup):
                inner = [
                    self._filter_to_odata(object_type, flt)
                    for flt in item.filters
                    if flt is not None
                ]
                inner = [clause for clause in inner if clause]
                if inner:
                    clauses.append(f"({' or '.join(inner)})")
            else:
                clause = self._filter_to_odata(object_type, item)
                if clause:
                    clauses.append(clause)
        return " and ".join(clauses)

    def _filter_to_odata(self, object_type: str, flt: Filter) -> str:
        field = self.mapper.to_native_field(object_type, flt.field)
        op = flt.op
        value = flt.value
        field_lower = str(field).lower()

        if isinstance(value, bool) and op in (FilterOp.EQ, FilterOp.NEQ):
            if flt.field == "IsClosed" and field_lower == "statecode":
                want_closed = value if op == FilterOp.EQ else not value
                return f"{field} ne 0" if want_closed else f"{field} eq 0"
            if flt.field == "IsConverted" and str(object_type).lower() == "lead":
                want_converted = value if op == FilterOp.EQ else not value
                return f"{field} eq 1" if want_converted else f"{field} ne 1"
            if flt.field == "IsActive" and field_lower == "isdisabled":
                want_active = value if op == FilterOp.EQ else not value
                return f"{field} eq {self._format_value(not want_active)}"

        if op == FilterOp.IS_NULL:
            return f"{field} eq null"
        if op == FilterOp.IS_NOT_NULL:
            return f"{field} ne null"
        if op == FilterOp.BEFORE_TODAY:
            start = self._utc_today_start()
            return f"{field} lt {self._format_datetime(start)}"
        if op == FilterOp.LAST_N_DAYS:
            cutoff = self._utc_now() - timedelta(days=int(value))
            return f"{field} ge {self._format_datetime(cutoff)}"
        if op == FilterOp.NOT_LAST_N_DAYS:
            cutoff = self._utc_now() - timedelta(days=int(value))
            return f"{field} lt {self._format_datetime(cutoff)}"

        if op == FilterOp.IN:
            values = list(value or [])
            if not values:
                return f"{field} eq null"
            if len(values) <= 200:
                return self._format_in_function(field, values)
            self._warnings.append(
                f"Dataverse IN filter for {field} expanded to OR chain ({len(values)} values)."
            )
            return self._format_or_chain(field, values)

        if op == FilterOp.NOT_IN:
            values = list(value or [])
            if not values:
                return f"{field} ne null"
            parts = [f"{field} ne {self._format_value(v)}" for v in values]
            return " and ".join(parts)

        op_map = {
            FilterOp.EQ: "eq",
            FilterOp.NEQ: "ne",
            FilterOp.LT: "lt",
            FilterOp.LTE: "le",
            FilterOp.GT: "gt",
            FilterOp.GTE: "ge",
        }
        if op in op_map:
            if value is None:
                return f"{field} eq null" if op == FilterOp.EQ else f"{field} ne null"
            return f"{field} {op_map[op]} {self._format_value(value)}"

        raise ValueError(f"Unsupported filter op: {op}")

    def _format_in_function(self, field: str, values: List[Any]) -> str:
        formatted = ", ".join(self._format_value(v) for v in values)
        return (
            "Microsoft.Dynamics.CRM.In("
            f"PropertyName='{field}',PropertyValues=[{formatted}]"
            ")"
        )

    def _format_or_chain(self, field: str, values: List[Any]) -> str:
        chunk_size = 200
        chunks = [values[i : i + chunk_size] for i in range(0, len(values), chunk_size)]
        parts: List[str] = []
        for chunk in chunks:
            inner = " or ".join(
                f"{field} eq {self._format_value(value)}" for value in chunk
            )
            if len(chunk) > 1:
                parts.append(f"({inner})")
            else:
                parts.append(inner)
        return " or ".join(parts)

    def _format_value(self, value: Any) -> str:
        if isinstance(value, bool):
            return "true" if value else "false"
        if isinstance(value, (int, float)):
            return str(value)
        if isinstance(value, datetime):
            return self._format_datetime(value)
        escaped = str(value).replace("'", "''")
        return f"'{escaped}'"

    def _format_datetime(self, value: datetime) -> str:
        dt = value.astimezone(timezone.utc).replace(microsecond=0)
        iso = dt.isoformat()
        if iso.endswith("+00:00"):
            iso = iso.replace("+00:00", "Z")
        return iso

    def _utc_now(self) -> datetime:
        return datetime.now(timezone.utc)

    def _utc_today_start(self) -> datetime:
        now = self._utc_now()
        return datetime(year=now.year, month=now.month, day=now.day, tzinfo=timezone.utc)

    def _parse_count(self, value: Any) -> int:
        if value is None:
            return 0
        try:
            return int(float(value))
        except Exception:
            return 0

    def _extract_label(self, display_name: Any) -> str:
        if isinstance(display_name, dict):
            user_label = display_name.get("UserLocalizedLabel") or {}
            if isinstance(user_label, dict):
                label = user_label.get("Label")
                if label:
                    return str(label)
            labels = display_name.get("LocalizedLabels") or []
            if labels:
                first = labels[0]
                if isinstance(first, dict):
                    label = first.get("Label")
                    if label:
                        return str(label)
        if isinstance(display_name, str):
            return display_name
        return ""

    def _format_query(
        self,
        entity: str,
        filter_expr: str,
        *,
        top: int | None = None,
        count: bool = False,
        orderby: str | None = None,
        select: List[str] | None = None,
    ) -> str:
        parts = [f"entity={entity}"]
        if select:
            parts.append(f"select={','.join(select)}")
        if filter_expr:
            parts.append(f"filter={filter_expr}")
        if orderby:
            parts.append(f"orderby={orderby}")
        if top is not None:
            parts.append(f"top={top}")
        if count:
            parts.append("count=true")
        return "dynamics." + " ".join(parts)


__all__ = ["DynamicsProvider"]
