"""Pydantic models shared across modules."""

from __future__ import annotations

from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from pydantic import BaseModel, Field, field_validator


class AzureEnvironment(str, Enum):
    """Supported Azure clouds."""

    AZURE_PUBLIC = "azure_public"
    AZURE_GOV = "azure_gov"
    AZURE_CHINA = "azure_china"
    AZURE_GERMANY = "azure_germany"
    AZURE_STACK = "azure_stack"


class AzureEnvironmentConfig(BaseModel):
    """Concrete endpoints for an Azure cloud."""

    name: AzureEnvironment
    authority_host: str
    resource_manager: str
    graph_endpoint: str
    storage_suffix: str


class ComplianceControl(BaseModel):
    """Individual compliance control reference."""

    control_id: str
    control_name: Optional[str] = None
    standard: str  # e.g., "NIST SP 800-53 Rev. 5", "CMMC 2.0", "ISO 27001"
    description: Optional[str] = None


class PolicyControlEvidence(BaseModel):
    """Explainability payload: which policy (and initiative reference) contributed a control."""

    standard: str
    control_id: str

    policy_definition_id: str
    policy_definition_reference_id: Optional[str] = None

    policy_definition_display_name: Optional[str] = None
    policy_definition_description: Optional[str] = None


class PolicyStateEvidence(BaseModel):
    """Explainability payload: noncompliance evidence from Policy Insights states."""

    policy_assignment_id: str
    policy_definition_id: Optional[str] = None
    policy_definition_reference_id: Optional[str] = None
    compliance_state: Optional[str] = None


class ComplianceMapping(BaseModel):
    """Compliance mapping for a resource via policy assignments."""

    policy_assignment_id: str
    policy_assignment_name: str

    policy_definition_id: Optional[str] = None
    policy_definition_display_name: Optional[str] = None
    policy_definition_description: Optional[str] = None

    initiative_id: Optional[str] = None
    initiative_name: Optional[str] = None

    controls: List[ComplianceControl] = Field(default_factory=list)

    # Optional explainability enrichments
    control_evidence: List[PolicyControlEvidence] = Field(default_factory=list)
    noncompliance_evidence: List[PolicyStateEvidence] = Field(default_factory=list)

    # Flattened helpers (derived from noncompliance_evidence)
    noncompliant_reference_ids: List[str] = Field(default_factory=list)
    noncompliant_policy_definition_ids: List[str] = Field(default_factory=list)

    compliance_state: Optional[str] = None  # Compliant, NonCompliant, Unknown


class PolicyInitiativeInfo(BaseModel):
    """Minimal initiative (policy set) metadata exposed to users."""

    id: str
    name: str
    display_name: Optional[str] = None
    category: Optional[str] = None
    policy_type: Optional[str] = None


class DiscoveryFilter(BaseModel):
    """Optional filters used to scope discovery."""

    include_types: Optional[List[str]] = None
    exclude_types: Optional[List[str]] = None
    required_tags: Optional[Dict[str, str]] = None
    resource_groups: Optional[List[str]] = None


class VisualizationOptions(BaseModel):
    """Graph visualization configuration."""

    output_dir: Path = Field(default=Path("artifacts/graphs"))
    file_name: Optional[str] = None
    include_attributes: bool = True
    physics_enabled: bool = True

    @field_validator("output_dir", mode="before")
    @classmethod
    def _ensure_path(cls, value: Any) -> Path:
        if value is None:
            raise ValueError("output_dir cannot be None")
        return Path(value)


class ResourceNode(BaseModel):
    """Normalized node in the discovery graph."""

    id: str
    name: str
    type: str
    subscription_id: str

    location: Optional[str] = None
    resource_group: Optional[str] = None

    tags: Dict[str, Any] = Field(default_factory=dict)
    properties: Dict[str, Any] = Field(default_factory=dict)
    dependencies: List[str] = Field(default_factory=list)
    compliance_mappings: List[ComplianceMapping] = Field(default_factory=list)


class ResourceRelationship(BaseModel):
    """Directed edge between two nodes."""

    source_id: str
    target_id: str
    relation_type: str
    weight: float = 1.0


class VisualizationResponse(BaseModel):
    """Output of rendering a graph visualization."""

    html_path: str
    nodes: int
    relationships: int


class DiscoveryPhaseMetrics(BaseModel):
    """Per-phase runtime stats for the orchestrator."""

    name: str
    elapsed_seconds: float
    nodes_added: int = 0
    relationships_added: int = 0


class DiscoveryMergeMetrics(BaseModel):
    """Relationship merge diagnostics."""

    dropped_dangling_relationships: int = 0
    deduped_relationships: int = 0


class DiscoveryMetrics(BaseModel):
    """Optional metrics payload included when requested."""

    phases: List[DiscoveryPhaseMetrics] = Field(default_factory=list)
    merge: DiscoveryMergeMetrics = Field(default_factory=DiscoveryMergeMetrics)


class AzureDiscoveryResponse(BaseModel):
    """Discovery output payload."""

    tenant_id: str
    discovered_subscriptions: List[str] = Field(default_factory=list)

    nodes: List[ResourceNode] = Field(default_factory=list)
    relationships: List[ResourceRelationship] = Field(default_factory=list)

    total_resources: int = 0
    html_report_path: Optional[str] = None

    metrics: Optional[DiscoveryMetrics] = None
    generated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class ScaleControlConfig(BaseModel):
    """Advanced scaling and throttling configuration."""

    enabled: bool = True
    initial_rps: float = Field(default=10.0, ge=0.1, le=100.0)
    min_rps: float = Field(default=1.0, ge=0.1, le=100.0)
    max_rps: float = Field(default=50.0, ge=1.0, le=200.0)
    max_concurrent_batches: int = Field(default=5, ge=1, le=20)
    adaptive_batching: bool = True
    initial_batch_size: int = Field(default=1000, ge=10, le=10000)


class DefenderConfig(BaseModel):
    """Defender for Cloud enumeration configuration."""

    include_security_alerts: bool = True
    include_security_assessments: bool = True
    include_secure_scores: bool = True
    include_compliance_status: bool = False  # Not yet implemented

    # Alert filtering
    alert_severity_filter: Optional[List[str]] = None  # ["High", "Medium", "Low", "Informational"]
    alert_status_filter: Optional[List[str]] = None  # ["Active", "Resolved", "Dismissed"]

    # Assessment filtering
    assessment_severity_filter: Optional[List[str]] = None  # ["High", "Medium", "Low"]
    assessment_status_filter: Optional[List[str]] = None  # ["Healthy", "Unhealthy", "NotApplicable"]


class M365Config(BaseModel):
    """Microsoft 365 enumeration configuration."""

    # What to enumerate
    include_sharepoint_sites: bool = True
    include_teams: bool = True
    include_onedrive: bool = True
    include_exchange_mailboxes: bool = False  # Requires Exchange.Manage or Mail.Read.All

    # SharePoint filtering
    sharepoint_site_filter: Optional[List[str]] = None  # Filter by site URL patterns
    sharepoint_max_sites: int = Field(default=1000, ge=1, le=50000)

    # Teams filtering
    teams_filter_archived: bool = True  # Exclude archived teams
    teams_max_teams: int = Field(default=1000, ge=1, le=50000)

    # OneDrive filtering
    onedrive_user_filter: Optional[List[str]] = None  # Filter by user UPN
    onedrive_max_drives: int = Field(default=1000, ge=1, le=50000)

    # Exchange filtering (if enabled)
    exchange_user_filter: Optional[List[str]] = None  # Filter by user UPN
    exchange_max_mailboxes: int = Field(default=1000, ge=1, le=50000)

    # Relationship options
    include_site_owners: bool = True
    include_team_members: bool = True
    include_drive_permissions: bool = True


class PurviewConfig(BaseModel):
    """Microsoft Purview enumeration configuration.
    
    Purview provides data governance, compliance, and information protection capabilities.
    Requires specific Graph API permissions:
    - InformationProtectionPolicy.Read.All (sensitivity labels)
    - Policy.Read.All (DLP policies, retention policies)
    - RecordsManagement.Read.All (retention labels)
    """

    # What to enumerate
    include_sensitivity_labels: bool = True  # Information protection sensitivity labels
    include_dlp_policies: bool = True  # Data Loss Prevention policies
    include_retention_policies: bool = True  # Retention and deletion policies
    include_retention_labels: bool = True  # Retention labels
    include_information_protection_policies: bool = True  # Information protection policies

    # Sensitivity label filtering
    sensitivity_label_filter: Optional[List[str]] = None  # Filter by label name patterns
    sensitivity_max_labels: int = Field(default=1000, ge=1, le=10000)

    # DLP policy filtering
    dlp_policy_filter: Optional[List[str]] = None  # Filter by policy name patterns
    dlp_max_policies: int = Field(default=1000, ge=1, le=10000)
    dlp_include_disabled: bool = False  # Include disabled policies

    # Retention policy filtering
    retention_policy_filter: Optional[List[str]] = None  # Filter by policy name patterns
    retention_max_policies: int = Field(default=1000, ge=1, le=10000)
    retention_include_disabled: bool = False  # Include disabled policies

    # Retention label filtering
    retention_label_filter: Optional[List[str]] = None  # Filter by label name patterns
    retention_max_labels: int = Field(default=1000, ge=1, le=10000)

    # Relationship options
    include_label_assignments: bool = True  # Build Label -> Resource edges
    include_policy_assignments: bool = True  # Build Policy -> Resource edges


class AzureDiscoveryRequest(BaseModel):
    """Input payload for discovery operations."""

    tenant_id: str

    environment: AzureEnvironment = AzureEnvironment.AZURE_PUBLIC
    detect_environment: bool = False
    detect_timeout_seconds: float = Field(default=8.0, ge=1.0, le=60.0)

    subscriptions: Optional[List[str]] = None
    filter: Optional[DiscoveryFilter] = None

    include_entra: bool = False
    include_m365: bool = False
    include_purview: bool = False
    include_defender_cloud: bool = False
    include_compliance: bool = False

    # RBAC enumeration controls
    include_rbac_assignments: bool = False
    include_rbac_definitions: bool = False
    rbac_scope_filter: Optional[List[str]] = None

    include_metrics: bool = False
    materialize_missing_endpoints: bool = False

    # ARM/ResourceGraph scoping controls
    arm_max_objects: int = Field(default=0, ge=0, le=1000000)

    # Global Microsoft Graph scoping controls
    graph_total_max_objects: int = Field(default=0, ge=0, le=1000000)

    # Entra scoping controls
    entra_include_organization: bool = True
    entra_include_domains: bool = True

    entra_include_users: bool = True
    entra_include_groups: bool = True
    entra_include_applications: bool = True
    entra_include_conditional_access_policies: bool = True
    entra_include_risky_users: bool = True
    entra_max_objects: int = Field(default=0, ge=0, le=500000)

    # Relationship scoping controls (Graph)
    entra_group_membership_max_groups: int = Field(default=50, ge=0, le=5000)
    entra_group_membership_max_members_per_group: int = Field(default=200, ge=0, le=500000)

    entra_ownership_max_apps: int = Field(default=50, ge=0, le=5000)
    entra_ownership_max_owners_per_app: int = Field(default=50, ge=0, le=500000)

    entra_sp_ownership_max_sps: int = Field(default=50, ge=0, le=5000)
    entra_sp_ownership_max_owners_per_sp: int = Field(default=50, ge=0, le=500000)

    # M365 scoping controls (legacy - deprecated in favor of m365_config)
    m365_include_applications: bool = True
    m365_include_service_principals: bool = True
    m365_max_objects: int = Field(default=0, ge=0, le=500000)

    # Compliance mapping controls
    compliance_include_policy_assignments: bool = True
    compliance_include_policy_states: bool = True
    compliance_max_assignments_per_scope: int = Field(default=100, ge=0, le=1000)

    include_relationships: bool = True
    max_batch_size: int = Field(default=1000, ge=100, le=5000)
    throttle_delay_seconds: float = Field(default=0.05, ge=0.0, le=5.0)
    prefer_cli_credentials: bool = False

    visualization: VisualizationOptions = Field(default_factory=VisualizationOptions)

    # Scale control configuration
    scale_controls: ScaleControlConfig = Field(default_factory=ScaleControlConfig)

    # Defender for Cloud configuration
    defender_config: DefenderConfig = Field(default_factory=DefenderConfig)

    # Microsoft 365 configuration
    m365_config: M365Config = Field(default_factory=M365Config)

    # Microsoft Purview configuration
    purview_config: PurviewConfig = Field(default_factory=PurviewConfig)

    # Optional progress callback (runtime-only; excluded from serialization).
    # When set, called with short status strings during Resource Graph enumeration.
    on_progress: Optional[Callable[[str], None]] = Field(default=None, exclude=True)

    @field_validator("tenant_id")
    @classmethod
    def _validate_tenant(cls, value: str) -> str:
        if not value or not value.strip():
            raise ValueError("tenant_id is required")
        return value.strip()


# Alias for backwards compatibility
Relationship = ResourceRelationship
