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"
    dkim_auto_rotate: bool = False
    dkim_rotation_interval_days: int = Field(default=0, ge=0, le=3650)
    dkim_overlap_days: int = Field(default=0, ge=0, le=365)

    @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
    dkim_auto_rotate: Optional[bool] = None
    dkim_rotation_interval_days: Optional[int] = Field(default=None, ge=0, le=3650)
    dkim_overlap_days: Optional[int] = Field(default=None, ge=0, le=365)


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
    # Path only (never the key material). Useful for operators; key file itself stays on disk.
    dkim_private_path: Optional[str] = None
    dkim_previous_selector: Optional[str] = None
    dkim_previous_public_key: Optional[str] = None
    dkim_previous_private_path: Optional[str] = None
    dkim_rotated_at: Optional[datetime] = None
    dkim_previous_expires_at: Optional[datetime] = None
    dkim_auto_rotate: bool = False
    dkim_rotation_interval_days: int = 0
    dkim_overlap_days: int = 0
    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
    }

    @field_validator("dkim_auto_rotate", mode="before")
    @classmethod
    def default_dkim_auto_rotate(cls, value):
        return bool(value) if value is not None else False

    @field_validator("dkim_rotation_interval_days", "dkim_overlap_days", mode="before")
    @classmethod
    def default_dkim_days(cls, value):
        return int(value or 0)


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)
    # Per-mailbox send limits; 0 inherits the global service limits.
    send_rate_burst: int = Field(default=0, ge=0, le=1000000)
    send_rate_per_minute: int = Field(default=0, ge=0, le=1000000)
    send_rate_per_hour: int = Field(default=0, ge=0, le=1000000)
    send_rate_per_day: int = Field(default=0, ge=0, le=1000000)
    send_rate_per_week: int = Field(default=0, ge=0, le=1000000)
    send_rate_per_month: int = Field(default=0, ge=0, le=1000000)
    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)
    send_rate_burst: Optional[int] = Field(default=None, ge=0, le=1000000)
    send_rate_per_minute: Optional[int] = Field(default=None, ge=0, le=1000000)
    send_rate_per_hour: Optional[int] = Field(default=None, ge=0, le=1000000)
    send_rate_per_day: Optional[int] = Field(default=None, ge=0, le=1000000)
    send_rate_per_week: Optional[int] = Field(default=None, ge=0, le=1000000)
    send_rate_per_month: Optional[int] = Field(default=None, ge=0, le=1000000)
    is_active: Optional[bool] = None
    require_app_password: Optional[bool] = None
    mfa_hint: Optional[bool] = None

class AccountOut(BaseModel):
    id: int
    email: str
    domain_id: int
    quota_mb: int
    send_rate_burst: int = 0
    send_rate_per_minute: int = 0
    send_rate_per_hour: int = 0
    send_rate_per_day: int = 0
    send_rate_per_week: int = 0
    send_rate_per_month: int = 0
    # Resolved values actually enforced (global fill-ins applied).
    effective_send_rate_burst: int = 0
    effective_send_rate_per_minute: int = 0
    effective_send_rate_per_hour: int = 0
    effective_send_rate_per_day: int = 0
    effective_send_rate_per_week: int = 0
    effective_send_rate_per_month: int = 0
    require_app_password: bool = False
    mfa_hint: bool = False
    is_active: bool
    created_at: datetime

    model_config = {
        "from_attributes": True
    }

    @field_validator(
        "send_rate_burst",
        "send_rate_per_minute",
        "send_rate_per_hour",
        "send_rate_per_day",
        "send_rate_per_week",
        "send_rate_per_month",
        "effective_send_rate_burst",
        "effective_send_rate_per_minute",
        "effective_send_rate_per_hour",
        "effective_send_rate_per_day",
        "effective_send_rate_per_week",
        "effective_send_rate_per_month",
        mode="before",
    )
    @classmethod
    def default_send_limits(cls, value):
        return int(value or 0)


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",
    "message-size-limit",
    "smtp-recipient-limit",
    "smtp-client-connection-count-limit",
    "api-auth-fail-limit",
    "api-auth-window-seconds",
    "api-auth-block-seconds",
    "send-rate-per-minute",
    "send-rate-per-hour",
    "send-rate-per-day",
    "send-rate-per-week",
    "send-rate-per-month",
    "send-rate-burst",
    "rspamd-action-greylist",
    "rspamd-action-add-header",
    "rspamd-action-reject",
    "rspamd-greylist-delay",
    "rspamd-greylist-expire",
    "dkim-rotation-interval-days",
    "dkim-overlap-days",
    "dkim-auto-rotate-default",
})


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
    update_channel: Optional[Literal["stable", "nightly"]] = 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

    @field_validator("update_channel")
    @classmethod
    def validate_update_channel(cls, value: Optional[str]) -> Optional[str]:
        if value is None:
            return None
        channel = value.strip().lower()
        if channel not in {"stable", "nightly"}:
            raise ValueError("update_channel must be stable or nightly")
        return channel


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


# ---------------------------------------------------------------------------
# Auth / MFA
# ---------------------------------------------------------------------------


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

    username: str = Field(min_length=1, max_length=128)
    password: str = Field(min_length=1, max_length=256)


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

    challenge_id: str = Field(min_length=8, max_length=128)
    method: Literal["totp", "sms"] = "totp"
    code: str = Field(min_length=4, max_length=16)


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

    challenge_id: str = Field(min_length=8, max_length=128)


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

    access_token: Optional[str] = Field(default=None, max_length=512)


class MfaAdminStatusOut(BaseModel):
    mfa_enabled: bool
    methods: list[str]
    totp_enabled: bool
    totp_pending: bool = False
    sms_enabled: bool
    sms_phone_masked: Optional[str] = None
    sms_provider: str = "crisaleo"


class MfaTotpEnrollOut(BaseModel):
    secret: str
    provisioning_uri: str
    issuer: str


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

    code: str = Field(min_length=6, max_length=8)


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

    enabled: Optional[bool] = None
    phone: Optional[str] = Field(default=None, max_length=32)
    provider: Optional[Literal["crisaleo", "sms2sms"]] = None


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

    code: Literal["crisaleo", "sms2sms"]
    enabled: Optional[bool] = None
    base_url: Optional[str] = Field(default=None, max_length=512)
    client_id: Optional[str] = Field(default=None, max_length=255)
    client_secret: Optional[str] = Field(default=None, max_length=512)
    api_key: Optional[str] = Field(default=None, max_length=512)
    send_path: Optional[str] = Field(default=None, max_length=255)
    from_sender: Optional[str] = Field(default=None, max_length=64)
    route: Optional[Literal["internal", "global"]] = None
    clear_secrets: bool = False


class MfaSmsProviderOut(BaseModel):
    code: str
    enabled: bool
    base_url: str
    client_id: Optional[str] = None
    has_client_secret: bool = False
    has_api_key: bool = False
    send_path: str
    from_sender: Optional[str] = None
    route: str = "internal"
    label: str = ""


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

    label: str = Field(default="client", min_length=1, max_length=128)


class AppPasswordOut(BaseModel):
    id: int
    label: str
    created_at: datetime
    last_used_at: Optional[datetime] = None
    revoked_at: Optional[datetime] = None


class AppPasswordCreatedOut(AppPasswordOut):
    password: str
    note: str = "Store this password now; it will not be shown again."
