"""Pydantic schemas for User management endpoints."""

import re
import uuid
from datetime import datetime

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


# ── Username policy ─────────────────────────────────────────────────

_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_-]{3,50}$")
_RESERVED_USERNAMES = frozenset({
    "admin", "root", "system", "api", "null", "undefined",
    "administrator", "superuser", "aegis",
})


def _validate_username(username: str) -> str:
    """Validate username format and reject reserved names."""
    if not _USERNAME_RE.match(username):
        raise ValueError(
            "Username must be 3-50 characters, containing only "
            "letters, digits, underscores, and hyphens"
        )
    if username.lower() in _RESERVED_USERNAMES:
        raise ValueError(f"Username '{username}' is reserved")
    return username


# ── Password policy ─────────────────────────────────────────────────

_MIN_PASSWORD_LENGTH = 12

_PASSWORD_RULES: list[tuple[str, str]] = [
    (r"[A-Z]", "at least one uppercase letter"),
    (r"[a-z]", "at least one lowercase letter"),
    (r"[0-9]", "at least one digit"),
    (r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?`~]", "at least one special character"),
]


def _validate_password_strength(password: str) -> str:
    """Check that *password* satisfies the complexity policy.

    Rules:
    - Minimum 12 characters
    - At least one uppercase letter
    - At least one lowercase letter
    - At least one digit
    - At least one special character
    """
    errors: list[str] = []

    if len(password) < _MIN_PASSWORD_LENGTH:
        errors.append(f"must be at least {_MIN_PASSWORD_LENGTH} characters long")

    for pattern, description in _PASSWORD_RULES:
        if not re.search(pattern, password):
            errors.append(description)

    if errors:
        raise ValueError(
            "Password does not meet complexity requirements: " + "; ".join(errors)
        )

    return password


# ── Create ──────────────────────────────────────────────────────────

class UserCreate(BaseModel):
    """Payload for creating a new user."""

    username: str
    email: str | None = None
    password: str
    role: str = "viewer"

    @field_validator("username")
    @classmethod
    def username_format(cls, v: str) -> str:
        return _validate_username(v)

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        return _validate_password_strength(v)


# ── Update ──────────────────────────────────────────────────────────

class UserUpdate(BaseModel):
    """Payload for partially updating an existing user.
    Every field is optional so callers send only what changed."""

    email: str | None = None
    role: str | None = None
    is_active: bool | None = None
    password: str | None = None

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str | None) -> str | None:
        if v is not None:
            return _validate_password_strength(v)
        return v


# ── Read (full) ─────────────────────────────────────────────────────

class PasswordChange(BaseModel):
    """Payload for changing the current user's password."""

    current_password: str
    new_password: str

    @field_validator("new_password")
    @classmethod
    def new_password_strength(cls, v: str) -> str:
        return _validate_password_strength(v)


class UserPreferencesUpdate(BaseModel):
    """Payload for updating current user's notification preferences and Jira/Tempo settings."""

    notification_preferences: dict | None = None
    jira_account_id: str | None = None
    # Personal Jira API token (Atlassian token) — write-only.
    # Set to empty string "" to clear the token.
    jira_api_token: str | None = None
    # Atlassian email for Jira auth — overrides account email.
    # Set to empty string "" to clear (falls back to account email).
    jira_email: str | None = None
    # Personal Tempo API token — write-only.
    # Set to empty string "" to clear the token.
    tempo_api_token: str | None = None


class UserOut(BaseModel):
    """Complete representation returned by the API."""

    id: uuid.UUID
    username: str
    email: str | None = None
    role: str
    is_active: bool
    must_change_password: bool = True
    created_at: datetime | None = None
    last_login: datetime | None = None
    notification_preferences: dict | None = None
    jira_account_id: str | None = None
    jira_email: str | None = None
    # Read from ORM but NEVER exposed in responses — used only to derive *_token_set flags.
    jira_api_token: str | None = Field(default=None, exclude=True)
    tempo_api_token: str | None = Field(default=None, exclude=True)
    # True when the user has the respective token stored.
    jira_token_set: bool = False
    tempo_token_set: bool = False

    model_config = ConfigDict(from_attributes=True)

    @model_validator(mode="after")
    def _derive_token_set_flags(self) -> "UserOut":
        """Derive *_token_set booleans from the (excluded) raw token fields.

        Uses @model_validator(mode='after') so Pydantic's Rust core calls it
        during FastAPI response serialisation — model_validate() overrides are
        bypassed by FastAPI's __pydantic_validator__.validate_python() path.
        """
        self.jira_token_set = bool(self.jira_api_token)
        self.tempo_token_set = bool(self.tempo_api_token)
        return self
