from datetime import datetime
from ipaddress import ip_address
import re
from typing import Literal, Optional

from pydantic import BaseModel, ConfigDict, EmailStr, Field, TypeAdapter, field_validator, model_validator

from .utils import normalize_domain, validate_local_part, validate_selector

ASN_RE = re.compile(r"^AS[0-9]{1,10}$")
HASH_RE = re.compile(r"^[A-Fa-f0-9]{8,}$")
DNS_ZONE_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
PANEL_TEXT_RE = re.compile(r"^[^\x00-\x1f\x7f]+$")
EMAIL_ADAPTER = TypeAdapter(EmailStr)
ReputationEntityType = Literal["ip", "domain", "email", "asn", "hash"]


class DomainBase(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    name: str
    is_active: bool = True
    max_users: int = Field(default=0, ge=0, le=1000000)
    dmarc_policy: Literal["none", "quarantine", "reject"] = Field(default="reject", description="none|quarantine|reject")

    @field_validator("name")
    @classmethod
    def validate_name(cls, value: str) -> str:
        return normalize_domain(value)

class DomainCreate(DomainBase):
    generate_dkim: bool = False
    dkim_selector: str = "default"

    @field_validator("dkim_selector")
    @classmethod
    def validate_dkim_selector(cls, value: str) -> str:
        return validate_selector(value)

class DomainUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    is_active: Optional[bool] = None
    max_users: Optional[int] = Field(default=None, ge=0, le=1000000)
    dmarc_policy: Optional[Literal["none", "quarantine", "reject"]] = None


class DomainDnsProviderUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    dns_provider: Optional[Literal["cloudflare", "none"]] = "cloudflare"
    dns_account_id: Optional[str] = None
    dns_zone_id: Optional[str] = None
    dns_api_token: Optional[str] = Field(default=None, min_length=1, max_length=4096)
    dns_sync_enabled: Optional[bool] = None
    clear_api_token: bool = False

    @field_validator("dns_account_id")
    @classmethod
    def validate_dns_account_id(cls, value: Optional[str]) -> Optional[str]:
        if value is None or value == "":
            return None
        if not DNS_ZONE_ID_RE.fullmatch(value):
            raise ValueError("Invalid DNS account ID")
        return value

    @field_validator("dns_zone_id")
    @classmethod
    def validate_dns_zone_id(cls, value: Optional[str]) -> Optional[str]:
        if value is None or value == "":
            return None
        if not DNS_ZONE_ID_RE.fullmatch(value):
            raise ValueError("Invalid DNS zone ID")
        return value

class DomainOut(DomainBase):
    id: int
    dkim_selector: Optional[str] = None
    dkim_public_key: Optional[str] = None
    dkim_private_path: Optional[str] = None
    dns_provider: Optional[str] = None
    dns_account_id: Optional[str] = None
    dns_zone_id: Optional[str] = None
    dns_sync_enabled: bool = False
    dns_last_sync_at: Optional[datetime] = None
    dns_last_sync_status: Optional[str] = None
    dns_has_api_token: bool = False
    created_at: datetime

    model_config = {
        "from_attributes": True
    }

class AccountBase(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    domain: str = Field(..., description="Domain name")
    local_part: str
    quota_mb: int = Field(default=2048, ge=1, le=1048576)
    is_active: bool = True

    @field_validator("domain")
    @classmethod
    def validate_domain(cls, value: str) -> str:
        return normalize_domain(value)

    @field_validator("local_part")
    @classmethod
    def validate_local(cls, value: str) -> str:
        return validate_local_part(value)

class AccountCreate(AccountBase):
    password: str = Field(min_length=12, max_length=256)

class AccountUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    password: Optional[str] = Field(default=None, min_length=12, max_length=256)
    quota_mb: Optional[int] = Field(default=None, ge=1, le=1048576)
    is_active: Optional[bool] = None

class AccountOut(BaseModel):
    id: int
    email: str
    domain_id: int
    quota_mb: int
    is_active: bool
    created_at: datetime

    model_config = {
        "from_attributes": True
    }

class AliasCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    domain: str
    source_local: str
    destination: EmailStr
    is_active: bool = True

    @field_validator("domain")
    @classmethod
    def validate_domain(cls, value: str) -> str:
        return normalize_domain(value)

    @field_validator("source_local")
    @classmethod
    def validate_source_local(cls, value: str) -> str:
        return validate_local_part(value)

    @field_validator("destination")
    @classmethod
    def normalize_destination(cls, value: EmailStr) -> str:
        return str(value).lower()

    @model_validator(mode="after")
    def prevent_self_alias(self) -> "AliasCreate":
        if f"{self.source_local}@{self.domain}" == self.destination:
            raise ValueError("Alias destination cannot match the source address")
        return self

class AliasOut(BaseModel):
    id: int
    source_local: str
    destination: str
    domain_id: int
    is_active: bool
    created_at: datetime

    model_config = {"from_attributes": True}


class AliasUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    is_active: Optional[bool] = None


class RedirectCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    domain: str
    source_local: str
    target_email: EmailStr
    is_active: bool = True

    @field_validator("domain")
    @classmethod
    def validate_domain(cls, value: str) -> str:
        return normalize_domain(value)

    @field_validator("source_local")
    @classmethod
    def validate_source_local(cls, value: str) -> str:
        return validate_local_part(value)

    @field_validator("target_email")
    @classmethod
    def normalize_target(cls, value: EmailStr) -> str:
        return str(value).lower()

    @model_validator(mode="after")
    def prevent_self_redirect(self) -> "RedirectCreate":
        if f"{self.source_local}@{self.domain}" == self.target_email:
            raise ValueError("Redirect target cannot match the source address")
        return self

class RedirectOut(BaseModel):
    id: int
    source_local: str
    target_email: str
    domain_id: int
    is_active: bool
    created_at: datetime

    model_config = {"from_attributes": True}


class RedirectUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    is_active: Optional[bool] = None


class ReputationCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    entity_type: ReputationEntityType
    entity_value: str
    score: float = Field(default=0, ge=-1000, le=1000)
    last_event: Optional[dict] = None

    @model_validator(mode="after")
    def validate_entity_value(self) -> "ReputationCreate":
        value = self.entity_value.strip()
        if self.entity_type == "ip":
            ip_address(value)
        elif self.entity_type == "domain":
            value = normalize_domain(value)
        elif self.entity_type == "email":
            value = str(EMAIL_ADAPTER.validate_python(value)).lower()
        elif self.entity_type == "asn":
            value = value.upper()
            if not ASN_RE.fullmatch(value):
                raise ValueError("Invalid ASN value")
        elif not HASH_RE.fullmatch(value):
            raise ValueError("Invalid hash value")
        self.entity_value = value
        return self

class ReputationUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    score: Optional[float] = Field(default=None, ge=-1000, le=1000)
    last_event: Optional[dict] = None

class ReputationOut(BaseModel):
    id: int
    entity_type: str
    entity_value: str
    score: float
    last_event: Optional[dict]
    updated_at: datetime

    model_config = {"from_attributes": True}


class AdminBanCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    ip: str
    reason: str = Field(default="manual", max_length=255)

    @field_validator("ip")
    @classmethod
    def validate_ip_value(cls, value: str) -> str:
        ip_address(value)
        return value


ADMIN_LIMIT_ALLOWED_KEYS = frozenset({
    "postfix-client-connection-rate-limit",
    "postfix-client-message-rate-limit",
    "postfix-rate-time-unit",
    "api-auth-fail-limit",
    "api-auth-window-seconds",
    "api-auth-block-seconds",
    "rspamd-action-greylist",
    "rspamd-action-add-header",
    "rspamd-action-reject",
    "rspamd-greylist-delay",
    "rspamd-greylist-expire",
})


class AdminLimitUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    key: str
    value: str

    @field_validator("key")
    @classmethod
    def validate_limit_key(cls, value: str) -> str:
        if value not in ADMIN_LIMIT_ALLOWED_KEYS:
            raise ValueError(f"Unknown limit key: {value}")
        return value


ADMIN_BACKUP_ALLOWED_KEYS = frozenset({
    "backup-enabled",
    "backup-type",
    "backup-full-weekday",
    "backup-db-mode",
    "backup-db-root-password",
    "backup-local-dir",
    "backup-retention-days",
    "backup-remote-targets",
    "backup-oncalendar",
    "backup-compression",
    "backup-include-redis",
    "backup-storage-type",
    "backup-storage-remote-name",
    "backup-storage-path",
    "backup-storage-host",
    "backup-storage-port",
    "backup-storage-user",
    "backup-storage-password",
    "backup-storage-bucket",
    "backup-storage-region",
    "backup-storage-endpoint",
    "backup-storage-access-key-id",
    "backup-storage-secret-access-key",
})


class AdminBackupUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    key: str
    value: str = ""

    @field_validator("key")
    @classmethod
    def validate_backup_key(cls, value: str) -> str:
        if value not in ADMIN_BACKUP_ALLOWED_KEYS:
            raise ValueError(f"Unknown backup key: {value}")
        return value


class AdminSettingsUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    server_name: Optional[str] = Field(default=None, min_length=1, max_length=128)
    panel_title: Optional[str] = Field(default=None, min_length=1, max_length=160)
    auto_updates_enabled: Optional[bool] = None

    @field_validator("server_name", "panel_title")
    @classmethod
    def validate_panel_text(cls, value: Optional[str]) -> Optional[str]:
        if value is not None and not PANEL_TEXT_RE.fullmatch(value):
            raise ValueError("Control characters are not allowed")
        return value


ADMIN_FIREWALL_ALLOWED_KEYS = frozenset({
    "firewall-enabled",
    "firewall-rules-json",
    "firewall-allowed-tcp-ports",
    "firewall-allowed-udp-ports",
})


class AdminFirewallUpdate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    key: str
    value: str = ""

    @field_validator("key")
    @classmethod
    def validate_firewall_key(cls, value: str) -> str:
        if value not in ADMIN_FIREWALL_ALLOWED_KEYS:
            raise ValueError(f"Unknown firewall key: {value}")
        return value
