import base64
import hashlib
import os
from datetime import datetime
from functools import cached_property
from typing import Annotated, Literal

import yaml
from pydantic import (
    UUID4,
    AnyUrl,
    Field,
    HttpUrl,
    MariaDBDsn,
    MongoDsn,
    MySQLDsn,
    PostgresDsn,
    RedisDsn,
    SecretStr,
    UrlConstraints,
    computed_field,
    model_validator,
)
from pydantic_extra_types.language_code import LanguageAlpha2
from pydantic_settings import BaseSettings, SettingsConfigDict

from vibetuner.logging import logger

from .paths import PathSettings, config_vars as config_vars_path


def _resolve_env_files() -> tuple[str, ...]:
    """Resolve .env file paths relative to the project root.

    Walks up from CWD to find the project root (like git finds .git/),
    then returns absolute paths to .env and .env.local there.
    Falls back to relative paths if no project root is found.
    """
    root = PathSettings._find_project_root()
    if root is None:
        return (".env", ".env.local")
    return (str(root / ".env"), str(root / ".env.local"))


_ENV_FILES = _resolve_env_files()


class SQLiteDsn(AnyUrl):
    """A type that will accept any SQLite DSN.

    * User info not required
    * TLD not required
    * Host not required (file-based database)
    """

    _constraints = UrlConstraints(
        allowed_schemes=[
            "sqlite",
            "sqlite+aiosqlite",
            "sqlite+pysqlite",
        ],
        host_required=False,
    )


current_year: int = datetime.now().year

UnprivilegedPort = Annotated[int, Field(ge=1024, le=65535)]


class SecurityHeadersSettings(BaseSettings):
    """Settings for built-in security headers middleware.

    All CSP extra_* fields accept space-separated source expressions that are
    appended to the corresponding CSP directive.
    """

    enabled: bool = True
    extra_script_src: str = ""
    extra_style_src: str = ""
    extra_font_src: str = ""
    extra_connect_src: str = ""
    extra_img_src: str = ""
    extra_media_src: str = ""
    frame_ancestors: str = "'self'"

    model_config = SettingsConfigDict(
        case_sensitive=False,
        extra="ignore",
        env_prefix="CSP_",
        env_file=_ENV_FILES,
    )


class RateLimitSettings(BaseSettings):
    """Settings for rate limiting middleware.

    Rate limit strings follow the format: "X per Y" or "X/Y"
    where X is the number and Y is the period (second, minute, hour, day).
    Examples: "10/minute", "100/hour", "5 per second"
    """

    enabled: bool = True
    default_limits: list[str] = []
    headers_enabled: bool = True
    strategy: str = "fixed-window"
    swallow_errors: bool = True

    model_config = SettingsConfigDict(
        case_sensitive=False,
        extra="ignore",
        env_prefix="RATE_LIMIT_",
        env_file=_ENV_FILES,
    )


class LocaleDetectionSettings(BaseSettings):
    """Settings for locale detection selectors.

    All selectors are enabled by default. The order is fixed:
    1. query_param - ?l=ca query parameter
    2. url_prefix - /ca/... path prefix
    3. user_session - authenticated user's stored preference
    4. cookie - language cookie
    5. accept_language - browser Accept-Language header
    """

    query_param: bool = True
    url_prefix: bool = True
    user_session: bool = True
    cookie: bool = True
    accept_language: bool = True

    model_config = SettingsConfigDict(
        case_sensitive=False,
        extra="ignore",
        env_prefix="LOCALE_",
        env_file=_ENV_FILES,
    )


class MailSettings(BaseSettings):
    """Mail provider configuration. Read from MAIL_* env vars."""

    provider: Literal["resend", "mailjet"] | None = None

    # Resend
    resend_api_key: SecretStr | None = None

    # Mailjet
    mailjet_api_key: SecretStr | None = None
    mailjet_api_secret: SecretStr | None = None

    model_config = SettingsConfigDict(
        case_sensitive=False,
        extra="ignore",
        env_prefix="MAIL_",
        env_file=_ENV_FILES,
    )


def _load_project_config() -> "ProjectConfiguration":
    if config_vars_path is None or not config_vars_path.exists():
        return ProjectConfiguration()

    yaml_data = yaml.safe_load(config_vars_path.read_text(encoding="utf-8"))
    return ProjectConfiguration(**yaml_data)


class ProjectConfiguration(BaseSettings):
    @classmethod
    def from_project_config(cls) -> "ProjectConfiguration":
        return _load_project_config()

    project_slug: str = "default_project"
    project_name: str = "default_project"

    project_description: str = "A default project description."

    # Language Related Settings
    supported_languages: set[LanguageAlpha2] | None = None
    default_language: LanguageAlpha2 = LanguageAlpha2("en")

    # AWS Parameters
    aws_default_region: str = "eu-central-1"

    # Company Name
    company_name: str = "Acme Corp"

    # From Email for transactional emails
    from_email: str = "no-reply@example.com"

    # Copyright
    copyright_start: Annotated[int, Field(strict=True, gt=1714, lt=2048)] = current_year

    # Analytics
    umami_website_id: UUID4 | None = None

    # Fully Qualified Domain Name
    fqdn: str | None = None

    @cached_property
    def languages(self) -> set[str]:
        if self.supported_languages is None:
            return {self.language}

        return {
            str(lang) for lang in (*self.supported_languages, self.default_language)
        }

    @cached_property
    def language(self) -> str:
        return str(self.default_language)

    @cached_property
    def copyright(self) -> str:
        year_part = (
            f"{self.copyright_start}-{current_year}"
            if self.copyright_start and self.copyright_start != current_year
            else str(current_year)
        )
        return f"© {year_part}{f' {self.company_name}' if self.company_name else ''}"

    model_config = SettingsConfigDict(case_sensitive=False, extra="ignore")


class CoreConfiguration(BaseSettings):
    project: ProjectConfiguration = ProjectConfiguration.from_project_config()

    debug: bool = False
    environment: Literal["dev", "prod"] = "dev"
    session_key: SecretStr = SecretStr("ct-!secret-must-change-me")

    # Database and Cache URLs
    mongodb_url: MongoDsn | None = None
    redis_url: RedisDsn | None = None
    database_url: PostgresDsn | MariaDBDsn | MySQLDsn | SQLiteDsn | None = None

    # Mail provider settings (MAIL_* env vars)
    mail: MailSettings = Field(default_factory=MailSettings)

    r2_default_bucket_name: str | None = None
    r2_bucket_endpoint_url: HttpUrl | None = None
    r2_access_key: SecretStr | None = None
    r2_secret_key: SecretStr | None = None
    r2_default_region: str = "auto"

    # OAuth provider credentials (read from env / .env)
    google_client_id: SecretStr | None = None
    google_client_secret: SecretStr | None = None
    github_client_id: SecretStr | None = None
    github_client_secret: SecretStr | None = None

    # Passphrase for Fernet-encrypting fields at rest in MongoDB.
    # Any Beanie document field typed as ``EncryptedStr`` (via EncryptedFieldsMixin)
    # is transparently encrypted on save and decrypted on load when this key is set.
    field_encryption_key: str | None = None

    worker_concurrency: int = 16

    # Port configuration (read from DEV_PORT / WORKER_PORT env or .env.local)
    dev_port: UnprivilegedPort | None = None
    worker_port: UnprivilegedPort | None = None

    @staticmethod
    def _compute_auto_port(path: str | None = None) -> int:
        """Deterministic port from directory path. Range: 10000-13999."""
        target_path = path or os.getcwd()
        hash_bytes = hashlib.sha256(target_path.encode()).digest()
        hash_int = int.from_bytes(hash_bytes[:4], "big")
        return 10000 + (hash_int % 4000)

    @computed_field
    @cached_property
    def resolved_port(self) -> int:
        """Frontend port: DEV_PORT if set, auto-calculated in dev, 8000 in prod."""
        if self.dev_port is not None:
            return self.dev_port
        if self.environment == "prod":
            return 8000
        return self._compute_auto_port()

    @computed_field
    @cached_property
    def resolved_worker_port(self) -> int:
        """Worker port: WORKER_PORT if set, otherwise 10000 + resolved_port."""
        if self.worker_port is not None:
            return self.worker_port
        return 10000 + self.resolved_port

    @computed_field
    @cached_property
    def version(self) -> str:
        """Project version computed lazily from git or package metadata."""
        from vibetuner.versioning import get_version

        return get_version()

    # Rate limiting settings
    rate_limit: RateLimitSettings = Field(default_factory=RateLimitSettings)

    # Locale detection settings
    locale_detection: LocaleDetectionSettings = Field(
        default_factory=LocaleDetectionSettings
    )

    # Security headers settings
    security_headers: SecurityHeadersSettings = Field(
        default_factory=SecurityHeadersSettings
    )

    # Proxy configuration for X-Forwarded-For/Proto headers
    # Comma-separated list of trusted proxy IPs/CIDRs (e.g., "127.0.0.1,192.168.1.0/24")
    # SECURITY: Only IPs in this list can set forwarded headers. Use "*" to trust all (NOT recommended for production)
    trusted_proxy_hosts: str = "127.0.0.1"

    # OAuth relay URL for shared redirect URI across multiple local apps.
    # When set, OAuth flows use this stable URL instead of the app's own URL.
    # Example: "https://oauth.localdev.alltuner.com:28000"
    oauth_relay_url: HttpUrl | None = None

    # External URL where this app is reachable (e.g. via Tailscale expose).
    # Read from EXPOSE_URL env var. When unset, defaults to http://localhost:{port}.
    expose_url: HttpUrl | None = None

    @model_validator(mode="after")
    def _default_expose_url(self) -> "CoreConfiguration":
        if self.expose_url is None:
            self.expose_url = HttpUrl(f"http://localhost:{self.resolved_port}")
        return self

    @cached_property
    def trusted_proxy_hosts_list(self) -> list[str]:
        """Parse trusted proxy hosts into a list for Granian's proxy header wrapper."""
        return [h.strip() for h in self.trusted_proxy_hosts.split(",") if h.strip()]

    @computed_field
    @cached_property
    def v_hash(self) -> str:
        hash_object = hashlib.sha256(self.version.encode("utf-8"))
        hash_bytes = hash_object.digest()

        b64_hash = base64.urlsafe_b64encode(hash_bytes).decode("utf-8")

        url_safe_hash = b64_hash.rstrip("=")[:8]

        return url_safe_hash

    @property
    def workers_available(self) -> bool:
        return self.redis_url is not None

    @cached_property
    def mongo_dbname(self) -> str:
        return self.project.project_slug

    @cached_property
    def redis_key_prefix(self) -> str:
        """Returns the Redis key prefix for namespacing all Redis keys by project and environment.

        Format: "{project_slug}:{env}:" for dev, "{project_slug}:" for prod.
        """
        if self.environment == "dev":
            return f"{self.project.project_slug}:dev:"
        return f"{self.project.project_slug}:"

    model_config = SettingsConfigDict(
        case_sensitive=False, extra="ignore", env_file=_ENV_FILES
    )


settings = CoreConfiguration()


logger.info("Configuration loaded for project: {}", settings.project.project_name)
