"""
Microsoft Dynamics 365 CRM Integration Core Module
Mirrors SalesforceIntegration with identical public API
"""

import aiohttp
import base64
import hashlib
import secrets
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, field
import time

# Import helper modules - NOTE: These files need to be created/moved to the package
# For now, we'll handle the missing imports gracefully
try:
    from .dynamics_rate_limiter import DynamicsRateLimiter
except ImportError:
    # If the module doesn't exist yet, we'll define it inline (see end of file)
    DynamicsRateLimiter = None

try:
    from .dynamics_oauth_complete import DynamicsOAuthFlow
except ImportError:
    DynamicsOAuthFlow = None

try:
    from .dynamics_batch_implementation import execute_batch as execute_batch_impl
except ImportError:
    execute_batch_impl = None

logger = logging.getLogger(__name__)


@dataclass
class DynamicsCredentials:
    """Credentials for Dynamics 365 authentication"""

    client_id: str
    client_secret: Optional[str] = None
    tenant_id: str = ""
    resource: str = ""  # https://<org>.crm.dynamics.com
    access_token: Optional[str] = None
    refresh_token: Optional[str] = None
    token_expires_at: Optional[datetime] = None


@dataclass
class ApiLimits:
    """Track API rate limits"""

    requests_remaining: int = 6000
    window_reset_time: Optional[datetime] = None
    retry_after: Optional[int] = None


@dataclass
class DynamicsEntity:
    """Entity metadata"""

    logical_name: str
    display_name: str
    is_custom: bool = False
    entity_set_name: Optional[str] = None  # Added for Phase 2 fix
    attributes: Dict[str, Any] = field(default_factory=dict)


@dataclass
class DynamicsField:
    """Field metadata"""

    logical_name: str
    display_name: str
    attribute_type: str
    attribute_type_name: Optional[str] = None  # Added for Phase 4 fix
    is_required: bool = False
    is_valid_for_create: bool = True
    max_length: Optional[int] = None
    min_value: Optional[Any] = None
    max_value: Optional[Any] = None
    precision: Optional[int] = None
    scale: Optional[int] = None
    option_set: Optional[List[Dict[str, Any]]] = None


class DynamicsIntegration:
    """
    Core Dynamics 365 integration class
    Public API identical to SalesforceIntegration
    FULLY ENHANCED with all Phase 2-4 fixes
    """

    def __init__(self, credentials: DynamicsCredentials, audit_logger=None):
        self.credentials = credentials
        self.session: Optional[aiohttp.ClientSession] = None
        self.api_limits = ApiLimits()
        self._metadata_cache: Dict[str, Any] = {}
        self._entity_set_cache: Dict[str, str] = {}  # Entity set name cache
        self._cache_ttl = 3600  # 1 hour
        self._last_cache_update: Optional[datetime] = None

        # OAuth components
        self.oauth_flow = None  # Will be initialized when needed
        self._code_verifier: Optional[str] = None
        self._code_challenge: Optional[str] = None

        # Production rate limiter
        self.rate_limiter = DynamicsRateLimiter()

        # Audit integration
        self.audit = None
        if audit_logger:
            from .audit_bridge import DynamicsAuditIntegration

            self.audit = DynamicsAuditIntegration(audit_logger, self)

    async def __aenter__(self):
        """Async context manager entry"""
        self.session = aiohttp.ClientSession()
        if self.audit:
            await self.audit.start_session()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit"""
        if self.session:
            await self.session.close()
        if self.audit:
            await self.audit.end_session()

    def _generate_pkce_pair(self) -> Tuple[str, str]:
        """Generate PKCE code verifier and challenge"""
        verifier = (
            base64.urlsafe_b64encode(secrets.token_bytes(32))
            .decode("utf-8")
            .rstrip("=")
        )
        challenge = (
            base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("utf-8")).digest())
            .decode("utf-8")
            .rstrip("=")
        )
        return verifier, challenge

    async def authenticate_oauth(
        self, client_id: str, tenant_id: str, redirect_uri: Optional[str] = None
    ) -> bool:
        """
        Complete OAuth 2.0 authentication with Azure AD
        Now with full browser flow and redirect handling
        """
        start_time = time.time()

        try:
            # Initialize OAuth flow handler if needed
            if not self.oauth_flow:
                self.oauth_flow = DynamicsOAuthFlow(self)

            # Use complete OAuth implementation
            success = await self.oauth_flow.authenticate_oauth(
                client_id, tenant_id, redirect_uri
            )

            # Audit authentication attempt
            if self.audit:
                await self.audit.log_authentication(
                    success,
                    {
                        "method": "oauth",
                        "client_id": client_id,
                        "tenant_id": tenant_id,
                        "duration_ms": (time.time() - start_time) * 1000,
                    },
                )

            return success

        except Exception as e:
            logger.error(f"OAuth authentication failed: {str(e)}")

            if self.audit:
                await self.audit.log_authentication(
                    False,
                    {
                        "method": "oauth",
                        "error": str(e),
                        "duration_ms": (time.time() - start_time) * 1000,
                    },
                )

            return False

    async def exchange_code_for_token(self, auth_code: str, redirect_uri: str) -> bool:
        """Exchange authorization code for access token"""
        # Delegate to OAuth flow handler
        if not self.oauth_flow:
            self.oauth_flow = DynamicsOAuthFlow(self)

        return await self.oauth_flow.exchange_code_for_token(auth_code, redirect_uri)

    async def refresh_access_token(self) -> bool:
        """Refresh access token using refresh token with audit logging"""
        if not self.credentials.refresh_token:
            logger.error("No refresh token available")
            return False

        start_time = time.time()
        token_endpoint = f"https://login.microsoftonline.com/{self.credentials.tenant_id}/oauth2/v2.0/token"

        data = {
            "client_id": self.credentials.client_id,
            "grant_type": "refresh_token",
            "refresh_token": self.credentials.refresh_token,
            "scope": f"{self.credentials.resource}/.default offline_access",
        }

        if self.credentials.client_secret:
            data["client_secret"] = self.credentials.client_secret

        if not self.session:
            self.session = aiohttp.ClientSession()

        try:
            async with self.session.post(token_endpoint, data=data) as resp:
                if resp.status == 200:
                    token_data = await resp.json()
                    self.credentials.access_token = token_data["access_token"]

                    if "refresh_token" in token_data:
                        self.credentials.refresh_token = token_data["refresh_token"]

                    expires_in = token_data.get("expires_in", 3600)
                    self.credentials.token_expires_at = datetime.utcnow() + timedelta(
                        seconds=expires_in
                    )

                    # Audit successful refresh
                    if self.audit:
                        await self.audit.log_event(
                            "dynamics_token_refresh",
                            {
                                "success": True,
                                "new_expiry": self.credentials.token_expires_at.isoformat(),
                                "duration_ms": (time.time() - start_time) * 1000,
                            },
                        )

                    logger.info("Access token refreshed successfully")
                    return True
                else:
                    error_data = await resp.text()
                    logger.error(f"Token refresh failed: {error_data}")

                    if self.audit:
                        await self.audit.log_error("token_refresh_failed", error_data)

                    return False

        except Exception as e:
            logger.error(f"Token refresh error: {str(e)}")

            if self.audit:
                await self.audit.log_error("token_refresh_error", str(e))

            return False

    async def test_connection(self) -> Tuple[bool, str]:
        """Test connection to Dynamics 365"""
        try:
            # Test with WhoAmI endpoint
            headers = self._get_headers()
            url = f"{self.credentials.resource}/api/data/v9.2/WhoAmI"

            async with await self._make_request("GET", url, headers=headers) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    user_id = data.get("UserId", "Unknown")
                    return True, f"Connected successfully. User ID: {user_id}"
                else:
                    return False, f"Connection failed: HTTP {resp.status}"
        except Exception as e:
            return False, f"Connection error: {str(e)}"

    def _get_headers(self) -> Dict[str, str]:
        """Get standard headers for API requests"""
        return {
            "Authorization": f"Bearer {self.credentials.access_token}",
            "Accept": "application/json",
            "OData-MaxVersion": "4.0",
            "OData-Version": "4.0",
            "Content-Type": "application/json",
            "Prefer": "odata.include-annotations=*",
        }

    def _get_entity_set_name(self, entity_logical_name: str) -> str:
        """Get proper entity set name for API calls"""
        # Check cache first
        if entity_logical_name in self._entity_set_cache:
            return self._entity_set_cache[entity_logical_name]

        # Common irregular plurals
        irregular_plurals = {
            "person": "people",
            "systemuser": "systemusers",
            "businessunit": "businessunits",
            "customeraddress": "customeraddresses",
            "opportunity": "opportunities",
            "activityparty": "activityparties",
        }

        if entity_logical_name in irregular_plurals:
            return irregular_plurals[entity_logical_name]

        # Default to adding 's'
        return f"{entity_logical_name}s"

    async def _make_request(
        self, method: str, url: str, **kwargs
    ) -> aiohttp.ClientResponse:
        """Make API request with production rate limiting, retry logic, and audit"""
        start_time = time.time()
        endpoint = url.replace(self.credentials.resource, "")

        # Apply production rate limiting
        await self.rate_limiter.acquire()

        # Update headers
        headers = kwargs.get("headers", {})
        headers.update(self._get_headers())
        kwargs["headers"] = headers

        try:
            async with self.session.request(method, url, **kwargs) as resp:
                # Update rate limiter from response headers
                self.rate_limiter.update_from_headers(dict(resp.headers))

                # Calculate duration
                duration_ms = (time.time() - start_time) * 1000

                # Audit API call
                if self.audit:
                    await self.audit.log_api_call(
                        method=method,
                        endpoint=endpoint,
                        status_code=resp.status,
                        duration_ms=duration_ms,
                        error=None if resp.status < 400 else f"HTTP {resp.status}",
                    )

                # Handle rate limiting (no recursion)
                if resp.status == 429:
                    retry_after = int(resp.headers.get("Retry-After", 60))
                    logger.warning(
                        "Rate limited. Will be handled by rate limiter on next request"
                    )

                    # Rate limiter will handle the wait on next acquire
                    if self.audit:
                        await self.audit.log_event(
                            "dynamics_rate_limit_hit",
                            {"retry_after": retry_after, "endpoint": endpoint},
                        )

                return resp

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000

            if self.audit:
                await self.audit.log_api_call(
                    method=method,
                    endpoint=endpoint,
                    status_code=0,
                    duration_ms=duration_ms,
                    error=str(e),
                )

            raise

    async def _ensure_metadata_cache(self):
        """Ensure metadata cache is populated and fresh"""
        if (
            not self._metadata_cache
            or not self._last_cache_update
            or (datetime.utcnow() - self._last_cache_update).seconds > self._cache_ttl
        ):
            await self._refresh_metadata_cache()

    async def _refresh_metadata_cache(self):
        """Refresh entity and field metadata cache with entity set names"""
        # Fetch entity definitions with EntitySetName
        entities_url = f"{self.credentials.resource}/api/data/v9.2/EntityDefinitions"
        params = {
            "$select": "LogicalName,DisplayName,IsCustomEntity,EntitySetName,CollectionName,IsActivity",
            "$filter": "IsValidForAdvancedFind eq true",
        }

        async with await self._make_request("GET", entities_url, params=params) as resp:
            if resp.status == 200:
                data = await resp.json()
                self._metadata_cache["entities"] = data.get("value", [])
                self._last_cache_update = datetime.utcnow()

                # Build entity set cache
                for entity in data.get("value", []):
                    logical_name = entity["LogicalName"]
                    entity_set = (
                        entity.get("EntitySetName")
                        or entity.get("CollectionName")
                        or f"{logical_name}s"
                    )
                    self._entity_set_cache[logical_name] = entity_set

    async def get_entities(self, custom_only: bool = False) -> List[DynamicsEntity]:
        """Get list of entities with proper entity set names"""
        await self._ensure_metadata_cache()

        entities = []
        for entity_data in self._metadata_cache.get("entities", []):
            if custom_only and not entity_data.get("IsCustomEntity", False):
                continue

            # Extract entity set name from metadata
            entity_set_name = entity_data.get("EntitySetName", "")
            collection_name = entity_data.get("CollectionName", "")
            logical_name = entity_data["LogicalName"]

            # Cache the mapping
            self._entity_set_cache[logical_name] = (
                entity_set_name or collection_name or f"{logical_name}s"
            )

            # Create enhanced entity object
            entity = DynamicsEntity(
                logical_name=logical_name,
                display_name=entity_data.get("DisplayName", {})
                .get("UserLocalizedLabel", {})
                .get("Label", logical_name),
                is_custom=entity_data.get("IsCustomEntity", False),
                entity_set_name=entity_set_name
                or collection_name
                or f"{logical_name}s",
            )
            entities.append(entity)

        return entities

    async def get_entity_fields(self, entity_logical_name: str) -> List[DynamicsField]:
        """Get fields for a specific entity with enhanced metadata"""
        # Check cache first
        cache_key = f"fields_{entity_logical_name}"
        if cache_key in self._metadata_cache:
            return self._metadata_cache[cache_key]

        # Get entity set name
        entity_set = self._get_entity_set_name(entity_logical_name)

        # Fetch field metadata with enhanced properties
        fields_url = f"{self.credentials.resource}/api/data/v9.2/EntityDefinitions(LogicalName='{entity_logical_name}')/Attributes"
        params = {
            "$select": "LogicalName,DisplayName,AttributeType,AttributeTypeName,RequiredLevel,IsValidForCreate,MaxLength,MinValue,MaxValue,Precision,Scale",
            "$expand": "OptionSet($select=Options)",
        }

        fields = []
        async with await self._make_request("GET", fields_url, params=params) as resp:
            if resp.status == 200:
                data = await resp.json()

                for field_data in data.get("value", []):
                    # Create enhanced field object
                    field = DynamicsField(
                        logical_name=field_data["LogicalName"],
                        display_name=field_data.get("DisplayName", {})
                        .get("UserLocalizedLabel", {})
                        .get("Label", field_data["LogicalName"]),
                        attribute_type=field_data["AttributeType"],
                        attribute_type_name=field_data.get("AttributeTypeName", {}).get(
                            "Value"
                        ),
                        is_required=field_data.get("RequiredLevel", {}).get("Value")
                        == "ApplicationRequired",
                        is_valid_for_create=field_data.get("IsValidForCreate", True),
                        max_length=field_data.get("MaxLength"),
                        min_value=field_data.get("MinValue", {}).get("Value")
                        if field_data.get("MinValue")
                        else None,
                        max_value=field_data.get("MaxValue", {}).get("Value")
                        if field_data.get("MaxValue")
                        else None,
                        precision=field_data.get("Precision"),
                        scale=field_data.get("Scale"),
                    )

                    # Handle option sets
                    if "OptionSet" in field_data and field_data["OptionSet"]:
                        field.option_set = [
                            {
                                "value": opt["Value"],
                                "label": opt.get("Label", {})
                                .get("UserLocalizedLabel", {})
                                .get("Label", str(opt["Value"])),
                            }
                            for opt in field_data["OptionSet"].get("Options", [])
                        ]

                    fields.append(field)

                # Cache the results
                self._metadata_cache[cache_key] = fields

        return fields

    async def execute_batch(
        self, requests: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Execute batch API request using the complete multipart implementation
        """
        # Use the imported batch implementation
        return await execute_batch_impl(self, requests)

    # Helper method to check if GUID is valid
    def _is_valid_guid(self, value: str) -> bool:
        """Check if value is a valid GUID"""
        import re

        guid_pattern = re.compile(
            r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
        )
        return bool(guid_pattern.match(value))
