"""Base ODBC connection configuration."""

import re

from abc import ABC, abstractmethod
from typing import Any

from pydantic import Field, PrivateAttr, computed_field, model_validator

from data_exchange_agent.config.sections.connections.base import BaseConnectionConfig


# ODBC validation patterns (defined outside class to avoid Pydantic PrivateAttr wrapping)
# Matches DNS-valid characters (letters, numbers, hyphens, dots, underscores)
_HOST_DNS_PATTERN = re.compile(r"^[a-zA-Z0-9_\-\.]+$")
# Matches IPv4 format (does not validate ranges)
_HOST_IP_PATTERN = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
# Matches LocalDB format (e.g., (localdb)\MSSQLLocalDB)
_HOST_LOCALDB_PATTERN = re.compile(r"^\(localdb\)\\[a-zA-Z0-9_]+$", re.IGNORECASE)
# Matches named SQL Server instance format (e.g., sql777snow\SQLSERVER for Windows Auth)
_HOST_NAMED_INSTANCE_PATTERN = re.compile(r"^[a-zA-Z0-9_\-\.]+\\[a-zA-Z0-9_]+$")


class BaseODBCConnectionConfig(BaseConnectionConfig, ABC):
    """Configuration base class for ODBC connection settings."""

    driver_name: str = Field(..., description="Driver name")
    username: str = Field(default="", description="Database username")
    password: str = Field(default="", description="Database password")
    database: str = Field(..., description="Database name")
    host: str = Field(..., description="Database host address")
    port: int = Field(..., ge=1, le=65535, description="Database port number")
    extra_options: dict[str, str | int | float | bool] = Field(
        default_factory=dict,
        description="Extra options to add to the ODBC connection string as key-value pairs",
    )

    # Private attribute to cache the connection string
    _connection_string_cache: str | None = PrivateAttr(default=None)

    @computed_field
    @property
    def connection_string(self) -> str:
        """Get the ODBC connection string (computed from build_connection_string)."""
        if self._connection_string_cache is None:
            self._connection_string_cache = self.build_connection_string()
        return self._connection_string_cache

    @abstractmethod
    def build_connection_string(self) -> str:
        """Build the ODBC connection string."""
        pass

    @model_validator(mode="after")
    def validate_odbc_config(self) -> "BaseODBCConnectionConfig":
        """Validate the ODBC connection configuration."""
        self._validate_host()
        self._validate_extra_options()
        return self

    def _validate_host(self) -> None:
        """Validate the host."""
        if not (1 <= len(self.host) <= 255):
            raise ValueError("Hostname length must be between 1 and 255 characters.")

        # Check for LocalDB format first (e.g., (localdb)\MSSQLLocalDB)
        if _HOST_LOCALDB_PATTERN.fullmatch(self.host):
            return  # Valid LocalDB format

        # Check for named SQL Server instance format (e.g., sql777snow\SQLSERVER)
        if _HOST_NAMED_INSTANCE_PATTERN.fullmatch(self.host):
            return  # Valid named instance format

        # Check for IP address format (simple check)
        if _HOST_IP_PATTERN.fullmatch(self.host):
            return  # It's a valid IPv4 format

        # This checks if it contains only valid DNS characters and dots
        if not _HOST_DNS_PATTERN.fullmatch(self.host):
            raise ValueError(
                "Hostname contains invalid characters. Must only contain letters, numbers, "
                "hyphens, dots, and backslashes (for named instances)."
            )

        # If not an IP, LocalDB, or named instance, treat as a DNS name and check common pitfalls

        # No leading or trailing dots
        if self.host.startswith(".") or self.host.endswith("."):
            raise ValueError("DNS name cannot start or end with a dot.")

        # Check for empty labels (e.g., "host..name")
        if ".." in self.host:
            raise ValueError("DNS name cannot contain adjacent dots (empty label).")

        # Check for hyphens at start/end of any label (part between dots)
        labels = self.host.split(".")
        for label in labels:
            if label.startswith("-") or label.endswith("-"):
                raise ValueError("Labels (parts between dots) cannot start or end with a hyphen.")

    def _validate_extra_options(self) -> None:
        """Validate extra options."""
        for key, value in self.extra_options.items():
            # validate key
            if not isinstance(key, str):
                raise ValueError(f"Extra option keys must be strings, got {type(key).__name__}.")
            if not key or not key.strip():
                raise ValueError("Extra option key cannot be empty or whitespace.")

            # validate value
            if value is None:
                raise ValueError(f"Extra option '{key}' value cannot be None.")
            if isinstance(value, str) and not value.strip():
                raise ValueError(f"Extra option '{key}' value cannot be empty or whitespace.")

    def _validate_required_field(self, value: Any, field_name: str) -> str | None:
        """Validate a required field (kept for backward compatibility with subclasses)."""
        if value is None:
            return f"{field_name} value cannot be None."
        if isinstance(value, str):
            if not value:
                return f"{field_name} value cannot be empty."
            if not value.strip():
                return f"{field_name} value cannot contain only whitespace."
        return None

    def _repr_fields(self) -> str:
        """Get string representation of fields (for use in __repr__)."""
        extra_options_masked = {
            key: "***" if key == "password" else self._mask_sensitive_data(str(value)) for key, value in self.extra_options.items()
        }
        return (
            f"driver_name='{self.driver_name}', "
            f"username='{self.username}', "
            f"password='***', "
            f"host='{self.host}', "
            f"port={self.port}, "
            f"database='{self.database}', "
            f"extra_options={extra_options_masked}, "
            f"connection_string='***'"
        )

    def __repr__(self) -> str:
        """Return string representation of an ODBC connection configuration."""
        class_name = self.__class__.__name__
        return f"{class_name}({self._repr_fields()})"
