"""
Dynamics 365 Data Validation Module
Property-aware validation matching Salesforce structure
ENHANCED: AttributeTypeName checks, precision/scale validation, inactive state pre-fetch
"""

import re
from typing import Dict, List, Optional, Any, Set
from dataclasses import dataclass, field
import pandas as pd
import logging
from datetime import datetime
from decimal import Decimal, InvalidOperation

logger = logging.getLogger(__name__)


@dataclass
class ValidationError:
    """Single validation error"""

    row_index: int
    field: str
    value: Any
    error_type: (
        str  # "required", "length", "type", "format", "business_rule", "inactive"
    )
    message: str


@dataclass
class ValidationResult:
    """Result of validation matching Salesforce structure"""

    is_valid: bool
    errors: List[ValidationError] = field(default_factory=list)
    warnings: List[ValidationError] = field(default_factory=list)
    total_rows: int = 0
    valid_rows: int = 0
    invalid_rows: int = 0

    def add_error(
        self, row: int, field: str, value: Any, error_type: str, message: str
    ):
        """Add validation error"""
        self.errors.append(ValidationError(row, field, value, error_type, message))

    def add_warning(
        self, row: int, field: str, value: Any, error_type: str, message: str
    ):
        """Add validation warning"""
        self.warnings.append(ValidationError(row, field, value, error_type, message))


class DynamicsDataValidator:
    """
    Validates data against Dynamics 365 metadata
    Mirrors SalesforceDataValidator interface
    ENHANCED with AttributeTypeName checks and state validation
    """

    def __init__(self, integration):
        """
        Args:
            integration: DynamicsIntegration instance
        """
        self.integration = integration
        self._field_metadata_cache: Dict[str, List[Any]] = {}
        self._inactive_records_cache: Dict[str, Set[str]] = {}

    async def validate_for_workflow(
        self,
        df: pd.DataFrame,
        entity: str,
        op_type: str = "upsert",
        key_field: Optional[str] = None,
        check_inactive: bool = True,
    ) -> ValidationResult:
        """
        Validate DataFrame for workflow operation

        Args:
            df: DataFrame to validate
            entity: Entity logical name
            op_type: Operation type (create, update, upsert)
            key_field: Key field for upsert operations
            check_inactive: Whether to check for inactive records

        Returns:
            ValidationResult with errors and warnings
        """
        result = ValidationResult(is_valid=True, total_rows=len(df))

        # Get field metadata
        fields = await self._get_field_metadata(entity)
        field_map = {f.logical_name: f for f in fields}

        # Pre-fetch inactive records if needed
        if check_inactive and op_type in ["update", "upsert"] and key_field:
            await self._prefetch_inactive_records(entity, df, key_field)

        # Track invalid rows
        invalid_row_indices = set()

        # Validate each row
        for idx, row in df.iterrows():
            row_errors = 0

            # Check if updating inactive record
            if check_inactive and op_type in ["update", "upsert"] and key_field:
                key_value = row.get(key_field)
                if key_value and self._is_inactive_record(entity, str(key_value)):
                    result.add_error(
                        idx,
                        key_field,
                        key_value,
                        "inactive",
                        f"Cannot update inactive {entity} record",
                    )
                    row_errors += 1

            # Validate each field
            for col in df.columns:
                if col not in field_map:
                    result.add_warning(
                        idx,
                        col,
                        None,
                        "unknown_field",
                        f"Field '{col}' not found in entity metadata",
                    )
                    continue

                field_meta = field_map[col]
                value = row[col]

                # Skip null values for non-required fields
                if pd.isna(value) or value is None:
                    if field_meta.is_required and op_type in ["create", "upsert"]:
                        result.add_error(
                            idx,
                            col,
                            value,
                            "required",
                            f"Required field '{col}' is empty",
                        )
                        row_errors += 1
                    continue

                # Validate based on field type - now using AttributeTypeName
                validation_errors = await self._validate_field_value_enhanced(
                    value, field_meta, idx, col, result
                )
                for error in validation_errors:
                    result.add_error(idx, col, value, error["type"], error["message"])
                    row_errors += 1

            # Apply business rules
            business_errors = self._validate_business_rules(row, entity, op_type, idx)
            for error in business_errors:
                result.add_error(
                    idx,
                    error["field"],
                    error["value"],
                    "business_rule",
                    error["message"],
                )
                row_errors += 1

            if row_errors > 0:
                invalid_row_indices.add(idx)

        # Update summary
        result.invalid_rows = len(invalid_row_indices)
        result.valid_rows = result.total_rows - result.invalid_rows
        result.is_valid = result.invalid_rows == 0

        return result

    async def _get_field_metadata(self, entity: str) -> List[Any]:
        """Get cached field metadata"""
        if entity not in self._field_metadata_cache:
            self._field_metadata_cache[
                entity
            ] = await self.integration.get_entity_fields(entity)
        return self._field_metadata_cache[entity]

    async def _prefetch_inactive_records(
        self, entity: str, df: pd.DataFrame, key_field: str
    ):
        """Pre-fetch inactive record statuses for validation"""
        # Get unique key values
        key_values = df[key_field].dropna().unique().tolist()
        if not key_values:
            return

        # Build filter for inactive records
        entity_set = self.integration._get_entity_set_name(entity)
        inactive_records = set()

        # Process in batches to avoid URL length limits
        batch_size = 50
        for i in range(0, len(key_values), batch_size):
            batch = key_values[i : i + batch_size]

            # Build OData filter
            if self.integration._is_valid_guid(str(batch[0])):
                # GUID-based filter
                filter_parts = [f"{entity}id eq {value}" for value in batch]
            else:
                # Alternate key filter
                filter_parts = []
                for value in batch:
                    safe_value = str(value).replace("'", "''")
                    filter_parts.append(f"{key_field} eq '{safe_value}'")

            filter_str = " or ".join(filter_parts)

            # Query for inactive records
            url = f"{self.integration.credentials.resource}/api/data/v9.2/{entity_set}"
            params = {
                "$select": f"{key_field},statecode",
                "$filter": f"({filter_str}) and statecode eq 1",  # 1 = Inactive
            }

            try:
                async with await self.integration._make_request(
                    "GET", url, params=params
                ) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        for record in data.get("value", []):
                            key_val = record.get(key_field) or record.get(f"{entity}id")
                            if key_val:
                                inactive_records.add(str(key_val))
            except Exception as e:
                logger.warning(f"Failed to fetch inactive records: {e}")

        self._inactive_records_cache[entity] = inactive_records

    def _is_inactive_record(self, entity: str, key_value: str) -> bool:
        """Check if record is inactive"""
        return key_value in self._inactive_records_cache.get(entity, set())

    async def _validate_field_value_enhanced(
        self,
        value: Any,
        field_meta: Any,
        row_idx: int,
        col: str,
        result: ValidationResult,
    ) -> List[Dict[str, str]]:
        """Enhanced field validation with AttributeTypeName checks"""
        errors = []

        # Get the attribute type name for special handling
        type_name = getattr(field_meta, "attribute_type_name", None)

        # Check for Virtual and Calculated fields
        if type_name in ["Virtual", "Calculated"]:
            result.add_warning(
                row_idx,
                col,
                value,
                "read_only",
                f"Field '{col}' is {type_name} and cannot be updated",
            )
            return errors

        # Type validation based on AttributeType
        if field_meta.attribute_type == "String":
            if not isinstance(value, str):
                # Try to convert
                try:
                    value = str(value)
                except:
                    errors.append(
                        {"type": "type", "message": "Cannot convert value to string"}
                    )

            # Length validation
            if field_meta.max_length and len(str(value)) > field_meta.max_length:
                errors.append(
                    {
                        "type": "length",
                        "message": f"Value exceeds maximum length of {field_meta.max_length}",
                    }
                )

        elif field_meta.attribute_type in ["Integer", "BigInt"]:
            try:
                int_value = int(value)

                # Check range based on min/max values
                if (
                    hasattr(field_meta, "min_value")
                    and field_meta.min_value is not None
                ):
                    if int_value < field_meta.min_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Value {int_value} is below minimum {field_meta.min_value}",
                            }
                        )

                if (
                    hasattr(field_meta, "max_value")
                    and field_meta.max_value is not None
                ):
                    if int_value > field_meta.max_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Value {int_value} exceeds maximum {field_meta.max_value}",
                            }
                        )

            except (ValueError, TypeError):
                errors.append({"type": "type", "message": "Invalid integer value"})

        elif field_meta.attribute_type == "Decimal":
            try:
                # Use Decimal for precision handling
                decimal_value = Decimal(str(value))

                # Check precision and scale
                if hasattr(field_meta, "precision") and field_meta.precision:
                    # Get total digits and decimal places
                    sign, digits, exponent = decimal_value.as_tuple()
                    total_digits = len(digits)
                    decimal_places = -exponent if exponent < 0 else 0

                    if total_digits - decimal_places > field_meta.precision - (
                        field_meta.scale or 0
                    ):
                        errors.append(
                            {
                                "type": "precision",
                                "message": f"Value exceeds precision of {field_meta.precision}",
                            }
                        )

                    if (
                        hasattr(field_meta, "scale")
                        and field_meta.scale
                        and decimal_places > field_meta.scale
                    ):
                        errors.append(
                            {
                                "type": "scale",
                                "message": f"Value has more than {field_meta.scale} decimal places",
                            }
                        )

            except (ValueError, TypeError, InvalidOperation):
                errors.append({"type": "type", "message": "Invalid decimal value"})

        elif field_meta.attribute_type == "Money":
            try:
                money_value = float(value)

                # Check range
                if (
                    hasattr(field_meta, "min_value")
                    and field_meta.min_value is not None
                ):
                    if money_value < field_meta.min_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Money value below minimum {field_meta.min_value}",
                            }
                        )

                if (
                    hasattr(field_meta, "max_value")
                    and field_meta.max_value is not None
                ):
                    if money_value > field_meta.max_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Money value exceeds maximum {field_meta.max_value}",
                            }
                        )

            except (ValueError, TypeError):
                errors.append({"type": "type", "message": "Invalid money value"})

        elif field_meta.attribute_type == "Boolean":
            # Accept various boolean representations
            valid_true = [True, 1, "1", "true", "True", "TRUE", "yes", "Yes", "YES"]
            valid_false = [False, 0, "0", "false", "False", "FALSE", "no", "No", "NO"]

            if value not in valid_true + valid_false:
                errors.append(
                    {"type": "type", "message": f"Invalid boolean value: {value}"}
                )

        elif field_meta.attribute_type == "DateTime":
            if not isinstance(value, datetime):
                try:
                    # Try to parse string dates
                    parsed_date = pd.to_datetime(value)

                    # Check date range (Dynamics has specific limits)
                    min_date = datetime(1900, 1, 1)
                    max_date = datetime(9999, 12, 31)

                    if parsed_date < min_date or parsed_date > max_date:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Date must be between {min_date} and {max_date}",
                            }
                        )

                except:
                    errors.append({"type": "type", "message": "Invalid datetime value"})

        elif field_meta.attribute_type == "Picklist" or type_name == "Picklist":
            # Enhanced picklist validation
            if field_meta.option_set:
                valid_values = [opt["value"] for opt in field_meta.option_set]

                # Allow both numeric and label values
                valid_labels = [opt["label"] for opt in field_meta.option_set]

                if value not in valid_values and str(value) not in [
                    str(v) for v in valid_values
                ]:
                    # Check if it's a label instead of value
                    if str(value) not in valid_labels:
                        errors.append(
                            {
                                "type": "format",
                                "message": f"Invalid option value. Valid values: {valid_values} or labels: {valid_labels}",
                            }
                        )

        elif field_meta.attribute_type == "Lookup":
            # Lookup fields should be GUIDs
            if not self._is_valid_guid(str(value)):
                errors.append(
                    {
                        "type": "format",
                        "message": "Invalid GUID format for lookup field",
                    }
                )

        elif field_meta.attribute_type == "Uniqueidentifier":
            if not self._is_valid_guid(str(value)):
                errors.append({"type": "format", "message": "Invalid GUID format"})

        elif field_meta.attribute_type == "Double":
            try:
                float_value = float(value)

                # Check range if specified
                if (
                    hasattr(field_meta, "min_value")
                    and field_meta.min_value is not None
                ):
                    if float_value < field_meta.min_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Value below minimum {field_meta.min_value}",
                            }
                        )

                if (
                    hasattr(field_meta, "max_value")
                    and field_meta.max_value is not None
                ):
                    if float_value > field_meta.max_value:
                        errors.append(
                            {
                                "type": "range",
                                "message": f"Value exceeds maximum {field_meta.max_value}",
                            }
                        )

            except (ValueError, TypeError):
                errors.append({"type": "type", "message": "Invalid double/float value"})

        return errors

    def _is_valid_guid(self, value: str) -> bool:
        """Check if value is a valid GUID"""
        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))

    def _validate_business_rules(
        self, row: pd.Series, entity: str, op_type: str, row_idx: int
    ) -> List[Dict[str, Any]]:
        """Apply entity-specific business rules"""
        errors = []

        if entity == "contact":
            # Contacts must have either lastname or fullname
            if pd.isna(row.get("lastname")) and pd.isna(row.get("fullname")):
                errors.append(
                    {
                        "field": "lastname",
                        "value": None,
                        "message": "Contact must have either lastname or fullname",
                    }
                )

            # Email format validation
            if "emailaddress1" in row and not pd.isna(row["emailaddress1"]):
                email = str(row["emailaddress1"])
                if not re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", email):
                    errors.append(
                        {
                            "field": "emailaddress1",
                            "value": email,
                            "message": "Invalid email format",
                        }
                    )

            # Phone number format validation
            for phone_field in ["telephone1", "mobilephone", "telephone2"]:
                if phone_field in row and not pd.isna(row[phone_field]):
                    phone = str(row[phone_field])
                    # Remove common formatting characters
                    cleaned_phone = re.sub(r"[\s\-\(\)\+\.]", "", phone)
                    if cleaned_phone and not cleaned_phone.isdigit():
                        errors.append(
                            {
                                "field": phone_field,
                                "value": phone,
                                "message": "Phone number contains invalid characters",
                            }
                        )

        elif entity == "account":
            # Account must have name
            if pd.isna(row.get("name")):
                errors.append(
                    {
                        "field": "name",
                        "value": None,
                        "message": "Account name is required",
                    }
                )

            # Validate website URL format if provided
            if "websiteurl" in row and not pd.isna(row["websiteurl"]):
                url = str(row["websiteurl"])
                if url and not re.match(r"^https?://", url):
                    # Add protocol if missing
                    if not url.startswith(("http://", "https://", "ftp://")):
                        errors.append(
                            {
                                "field": "websiteurl",
                                "value": url,
                                "message": "Website URL should start with http:// or https://",
                            }
                        )

            # Validate email
            if "emailaddress1" in row and not pd.isna(row["emailaddress1"]):
                email = str(row["emailaddress1"])
                if not re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", email):
                    errors.append(
                        {
                            "field": "emailaddress1",
                            "value": email,
                            "message": "Invalid email format",
                        }
                    )

        elif entity == "lead":
            # Lead must have subject or fullname
            if pd.isna(row.get("subject")) and pd.isna(row.get("fullname")):
                # Check if firstname and lastname are provided instead
                if pd.isna(row.get("firstname")) or pd.isna(row.get("lastname")):
                    errors.append(
                        {
                            "field": "subject",
                            "value": None,
                            "message": "Lead must have either subject or name information",
                        }
                    )

            # Validate email
            if "emailaddress1" in row and not pd.isna(row["emailaddress1"]):
                email = str(row["emailaddress1"])
                if not re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", email):
                    errors.append(
                        {
                            "field": "emailaddress1",
                            "value": email,
                            "message": "Invalid email format",
                        }
                    )

        elif entity == "opportunity":
            # Opportunity must have name
            if pd.isna(row.get("name")):
                errors.append(
                    {
                        "field": "name",
                        "value": None,
                        "message": "Opportunity name is required",
                    }
                )

            # Validate estimated close date
            if "estimatedclosedate" in row and not pd.isna(row["estimatedclosedate"]):
                try:
                    close_date = pd.to_datetime(row["estimatedclosedate"])
                    # Warn if close date is in the past
                    if close_date.date() < datetime.now().date():
                        errors.append(
                            {
                                "field": "estimatedclosedate",
                                "value": close_date,
                                "message": "Estimated close date is in the past",
                            }
                        )
                except:
                    pass

        # Common rule: Cannot create/update with explicit statecode = 1 (inactive)
        if "statecode" in row and not pd.isna(row["statecode"]):
            if int(row["statecode"]) == 1 and op_type in ["create"]:
                errors.append(
                    {
                        "field": "statecode",
                        "value": row["statecode"],
                        "message": "Cannot create records in inactive state",
                    }
                )

        return errors

    async def get_validation_summary(self, result: ValidationResult) -> str:
        """Get human-readable validation summary"""
        summary = "Validation Results:\n"
        summary += f"- Total rows: {result.total_rows}\n"
        summary += f"- Valid rows: {result.valid_rows}\n"
        summary += f"- Invalid rows: {result.invalid_rows}\n"

        if result.errors:
            summary += f"\nErrors ({len(result.errors)}):\n"
            # Group errors by type
            error_types = {}
            for error in result.errors:
                if error.error_type not in error_types:
                    error_types[error.error_type] = []
                error_types[error.error_type].append(error)

            for error_type, errors in error_types.items():
                summary += f"  {error_type}: {len(errors)} errors\n"
                # Show first 3 examples
                for i, error in enumerate(errors[:3]):
                    summary += (
                        f"    - Row {error.row_index}, {error.field}: {error.message}\n"
                    )
                if len(errors) > 3:
                    summary += f"    ... and {len(errors) - 3} more\n"

        if result.warnings:
            summary += f"\nWarnings ({len(result.warnings)}):\n"
            for warning in result.warnings[:5]:
                summary += (
                    f"  - Row {warning.row_index}, {warning.field}: {warning.message}\n"
                )
            if len(result.warnings) > 5:
                summary += f"  ... and {len(result.warnings) - 5} more\n"

        return summary
