"""Reusable validators for Communication domain."""
from __future__ import annotations

import re
from typing import Any

from shared.exceptions import ValidationAppError

from app.models.types import (
    ChannelType,
    ContactSourceType,
    ProviderKind,
    QueuePriority,
    TemplateStatus,
)

PHONE_RE = re.compile(r"^\+?[0-9]{8,15}$")
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
TEMPLATE_VAR_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")


class ValidationError(ValidationAppError):
    """Local alias for Communication validators."""


def validate_channel(channel: str) -> str:
    try:
        return ChannelType(channel).value
    except ValueError as exc:
        raise ValidationError(
            f"Unsupported channel: {channel}",
            details={"allowed": [c.value for c in ChannelType]},
        ) from exc


def validate_provider_kind(kind: str) -> str:
    try:
        return ProviderKind(kind).value
    except ValueError as exc:
        raise ValidationError(
            f"Unsupported provider kind: {kind}",
            details={"allowed": [k.value for k in ProviderKind]},
        ) from exc


def validate_provider_payload(data: dict[str, Any]) -> None:
    if not data.get("name"):
        raise ValidationError("Provider name is required")
    validate_channel(data["channel"])
    validate_provider_kind(data["provider_kind"])
    priority = data.get("priority", 100)
    if not isinstance(priority, int) or priority < 1 or priority > 10000:
        raise ValidationError("priority must be an integer between 1 and 10000")
    rate = data.get("rate_limit_per_minute")
    if rate is not None and (not isinstance(rate, int) or rate < 1):
        raise ValidationError("rate_limit_per_minute must be a positive integer")


def validate_sender_number(data: dict[str, Any]) -> None:
    validate_channel(data["channel"])
    value = (data.get("value") or "").strip()
    if not value:
        raise ValidationError("Sender number value is required")
    if data["channel"] == ChannelType.SMS.value and not PHONE_RE.match(value):
        # Allow alphanumeric short codes for SMS
        if not re.match(r"^[A-Za-z0-9+\-]{3,32}$", value):
            raise ValidationError("Invalid sender number format")


def validate_template(data: dict[str, Any]) -> None:
    if not data.get("template_key"):
        raise ValidationError("template_key is required")
    if not data.get("name"):
        raise ValidationError("template name is required")
    if not data.get("body"):
        raise ValidationError("template body is required")
    validate_channel(data["channel"])
    locale = data.get("locale", "fa")
    if not isinstance(locale, str) or len(locale) < 2:
        raise ValidationError("locale must be a valid language code")
    declared = data.get("variables")
    if declared is not None and not isinstance(declared, list):
        raise ValidationError("variables must be a list")
    found = set(TEMPLATE_VAR_RE.findall(data["body"]))
    if declared is not None:
        missing = found - set(declared)
        if missing:
            raise ValidationError(
                "Template body references undeclared variables",
                details={"missing": sorted(missing)},
            )


def validate_template_status_transition(current: str, new: str) -> None:
    allowed = {
        TemplateStatus.DRAFT.value: {
            TemplateStatus.PENDING_APPROVAL.value,
            TemplateStatus.ARCHIVED.value,
        },
        TemplateStatus.PENDING_APPROVAL.value: {
            TemplateStatus.APPROVED.value,
            TemplateStatus.REJECTED.value,
            TemplateStatus.DRAFT.value,
        },
        TemplateStatus.APPROVED.value: {TemplateStatus.ARCHIVED.value},
        TemplateStatus.REJECTED.value: {
            TemplateStatus.DRAFT.value,
            TemplateStatus.ARCHIVED.value,
        },
        TemplateStatus.ARCHIVED.value: set(),
    }
    if new not in allowed.get(current, set()):
        raise ValidationError(f"Cannot transition template from {current} to {new}")


def validate_contact(data: dict[str, Any]) -> None:
    phone = data.get("phone")
    email = data.get("email")
    if not phone and not email and not data.get("push_token"):
        raise ValidationError("At least one of phone, email, or push_token is required")
    if phone and not PHONE_RE.match(phone):
        raise ValidationError("Invalid phone format")
    if email and not EMAIL_RE.match(email):
        raise ValidationError("Invalid email format")


def validate_contact_source(data: dict[str, Any]) -> None:
    if not data.get("name"):
        raise ValidationError("Contact source name is required")
    try:
        ContactSourceType(data["source_type"])
    except (KeyError, ValueError) as exc:
        raise ValidationError(
            "Invalid contact source type",
            details={"allowed": [s.value for s in ContactSourceType]},
        ) from exc
    source_type = data["source_type"]
    if source_type not in (ContactSourceType.MANUAL.value, ContactSourceType.CSV.value):
        if not data.get("base_url"):
            raise ValidationError("base_url is required for API contact sources")


def validate_message_send(data: dict[str, Any]) -> None:
    validate_channel(data["channel"])
    if not data.get("to_address") and not data.get("contact_source_id"):
        raise ValidationError("to_address or contact_source_id is required")
    if data.get("to_address") and data["channel"] == ChannelType.SMS.value:
        if not PHONE_RE.match(data["to_address"]):
            raise ValidationError("Invalid SMS destination phone")
    if not data.get("body") and not data.get("template_key") and not data.get("template_id"):
        raise ValidationError("body or template_key/template_id is required")
    priority = data.get("priority", QueuePriority.NORMAL.value)
    try:
        QueuePriority(priority)
    except ValueError as exc:
        raise ValidationError("Invalid priority") from exc


def validate_otp_request(data: dict[str, Any]) -> None:
    validate_channel(data.get("channel", ChannelType.SMS.value))
    destination = data.get("destination")
    if not destination:
        raise ValidationError("destination is required")
    channel = data.get("channel", ChannelType.SMS.value)
    if channel == ChannelType.SMS.value and not PHONE_RE.match(destination):
        raise ValidationError("Invalid OTP destination phone")
    if channel == ChannelType.EMAIL.value and not EMAIL_RE.match(destination):
        raise ValidationError("Invalid OTP destination email")


def validate_otp_verify(data: dict[str, Any]) -> None:
    if not data.get("destination"):
        raise ValidationError("destination is required")
    code = data.get("code")
    if not code or not str(code).isdigit() or not (4 <= len(str(code)) <= 8):
        raise ValidationError("code must be a 4-8 digit OTP")


def validate_webhook(data: dict[str, Any]) -> None:
    if not data.get("provider_kind"):
        raise ValidationError("provider_kind is required")
    if not data.get("event_type"):
        raise ValidationError("event_type is required")


def extract_template_variables(body: str) -> list[str]:
    return sorted(set(TEMPLATE_VAR_RE.findall(body)))


def render_template(body: str, variables: dict[str, Any] | None = None) -> str:
    variables = variables or {}
    found = TEMPLATE_VAR_RE.findall(body)

    def repl(match: re.Match[str]) -> str:
        key = match.group(1)
        if key not in variables:
            raise ValidationError(
                f"Missing template variable: {key}",
                details={"variable": key},
            )
        return str(variables[key])

    # Ensure all found vars are present when rendering
    missing = [k for k in found if k not in variables]
    if missing:
        raise ValidationError(
            "Missing template variables",
            details={"missing": missing},
        )
    return TEMPLATE_VAR_RE.sub(repl, body)
