"""SQL Server ODBC connection configuration."""

import logging
import re

from pydantic import Field, PrivateAttr, model_validator

from data_exchange_agent.config.sections.connections.odbc.base import (
    BaseODBCConnectionConfig,
)
from data_exchange_agent.constants.connection_types import ConnectionType
from shared.sqlserver.odbc_driver_utils import (
    get_best_sqlserver_driver,
    is_driver_available,
)


logger = logging.getLogger(__name__)


# Connection string templates for SQL Server ODBC
# Note: Encrypt and TrustServerCertificate are added conditionally if specified by user
SQLSERVER_CONNECTION_STRING = (
    "DRIVER={{{driver}}};"
    "SERVER={server},{port};"
    "DATABASE={database};"
    "UID={user};"
    "PWD={password}"
)

SQLSERVER_CONNECTION_STRING_NAMED_INSTANCE = (
    "DRIVER={{{driver}}};"
    "SERVER={server};"
    "DATABASE={database};"
    "UID={user};"
    "PWD={password}"
)

SQLSERVER_CONNECTION_STRING_WIN_AUTH = (
    "DRIVER={{{driver}}};"
    "SERVER={server},{port};"
    "DATABASE={database};"
    "Trusted_Connection=yes"
)

SQLSERVER_CONNECTION_STRING_WIN_AUTH_NAMED_INSTANCE = (
    "DRIVER={{{driver}}};"
    "SERVER={server};"
    "DATABASE={database};"
    "Trusted_Connection=yes"
)

# Pattern for named instances (e.g., "hostname\INSTANCENAME")
NAMED_INSTANCE_PATTERN = re.compile(r"^[a-zA-Z0-9.-]+\\[a-zA-Z0-9_]+$")

# Pattern for LocalDB instances (e.g., "(localdb)\MSSQLLocalDB")
LOCALDB_PATTERN = re.compile(r"^\(localdb\)\\[a-zA-Z0-9_]+$", re.IGNORECASE)

# Default SQL Server port
DEFAULT_SQL_SERVER_PORT = 1433

# Configuration keys
INSTANCE_NAME_KEY = "instanceName"
TRUSTED_CONNECTION_KEY = "Trusted_Connection"

# Default ODBC driver
DEFAULT_ODBC_DRIVER = "ODBC Driver 18 for SQL Server"


class SQLServerConnectionConfig(BaseODBCConnectionConfig):
    """
    Configuration class for SQL Server ODBC connection settings.

    Supports two authentication modes:
    1. SQL Server Authentication: Uses username and password
    2. Windows Authentication: Uses trusted connection (integrated security)

    For Windows Authentication, set use_windows_auth=True. In this mode,
    username and password are optional as the connection uses the Windows
    credentials of the process running the application.

    References:
        - https://learn.microsoft.com/en-us/sql/relational-databases/security/choose-an-authentication-mode
        - https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/connection-string-keywords-and-data-source-names-dsns

    """

    # Override parent fields with SQL Server-specific defaults
    driver_name: str = Field(
        default=ConnectionType.SQLSERVER, description="Driver name"
    )
    host: str = Field(default="localhost", description="Database host address")
    port: int = Field(
        default=DEFAULT_SQL_SERVER_PORT,
        ge=1,
        le=65535,
        description="Database port number",
    )
    username: str | None = Field(
        default=None, description="Database username (required for SQL Server Auth)"
    )
    password: str | None = Field(
        default=None, description="Database password (required for SQL Server Auth)"
    )

    # SQL Server specific fields
    use_windows_auth: bool = Field(
        default=False, description="Use Windows Authentication (trusted connection)"
    )
    use_bcp: bool = Field(
        default=False, description="Enable BCP (Bulk Copy Program) for data extraction"
    )
    requested_odbc_driver: str | None = Field(
        default=None,
        alias="odbc_driver",
        description="ODBC driver name (auto-detected if not specified)",
    )
    trust_server_certificate: bool | None = Field(
        default=None, description="Whether to trust the server certificate"
    )
    encrypt_bool: bool | None = Field(
        default=None, alias="encrypt", description="Whether to encrypt the connection"
    )
    auto_detect_driver: bool = Field(
        default=True, description="Auto-detect the best available ODBC driver"
    )

    # Private attributes for internal state
    _resolved_odbc_driver: str = PrivateAttr()
    _encrypt_odbc_string: str | None = PrivateAttr(default=None)
    _trust_server_certificate_odbc_string: str | None = PrivateAttr(default=None)

    @property
    def odbc_driver(self) -> str:
        """Get the resolved ODBC driver name."""
        return self._resolved_odbc_driver

    @property
    def encrypt(self) -> str | None:
        """Get the encrypt setting as ODBC string format."""
        return self._encrypt_odbc_string

    def model_post_init(self, __context) -> None:
        """Post-initialization setup."""
        # Convert boolean values to ODBC string format ("yes"/"no")
        self._encrypt_odbc_string = self._bool_to_odbc_string(self.encrypt_bool)
        self._trust_server_certificate_odbc_string = self._bool_to_odbc_string(
            self.trust_server_certificate
        )

        # Smart driver selection with auto-detection
        self._resolved_odbc_driver = self._select_odbc_driver(
            self.requested_odbc_driver, self.auto_detect_driver
        )

        # Validate encryption settings with the selected driver
        self._validate_encryption_compatibility(self.encrypt_bool)

    @model_validator(mode="after")
    def validate_sqlserver_config(self) -> "SQLServerConnectionConfig":
        """Validate the SQL Server connection configuration."""
        self._validate_authentication()
        self._validate_extra_options_sqlserver()
        return self

    def _validate_authentication(self) -> None:
        """Validate authentication configuration based on authentication mode."""
        if not self.use_windows_auth:
            # SQL Server Authentication requires username and password
            if not self.username or not self.username.strip():
                raise ValueError(
                    "Username is required for SQL Server Authentication. "
                    "Set 'use_windows_auth=True' to use Windows Authentication instead."
                )
            if not self.password:
                raise ValueError(
                    "Password is required for SQL Server Authentication. "
                    "Set 'use_windows_auth=True' to use Windows Authentication instead."
                )

    def _validate_extra_options_sqlserver(self) -> None:
        """Validate SQL Server specific extra options."""
        # Validate instance name if provided
        if INSTANCE_NAME_KEY in self.extra_options:
            instance_name_value = self.extra_options[INSTANCE_NAME_KEY]
            if not isinstance(instance_name_value, str):
                raise ValueError(
                    f"Instance name must be a string, got {type(instance_name_value).__name__}."
                )
            if not instance_name_value:
                raise ValueError("Instance name cannot be empty.")
            stripped_instance_name_value = instance_name_value.strip()
            if not stripped_instance_name_value:
                raise ValueError("Instance name cannot contain only whitespace.")
            if not re.fullmatch(
                r"[a-zA-Z][a-zA-Z0-9_$]*", stripped_instance_name_value
            ):
                raise ValueError(
                    "Instance name must start with a letter, and only contain"
                    " alphanumeric characters, underscores (_), and dollar signs ($)."
                )
            if len(stripped_instance_name_value) > 16:
                raise ValueError(
                    "Instance name length must be less than or equal to 16 characters."
                )

        # Warn if Trusted_Connection is passed as an extra option
        if TRUSTED_CONNECTION_KEY in self.extra_options:
            raise ValueError(
                f"'{TRUSTED_CONNECTION_KEY}' should not be passed as an extra option. "
                f"Use 'use_windows_auth=True' instead to enable Windows Authentication."
            )

    def build_connection_string(self) -> str:
        r"""
        Build the ODBC connection string for SQL Server.

        When Windows Authentication is enabled, uses Trusted_Connection=yes.
        Supports named instances (e.g., "hostname\INSTANCENAME").
        """
        instance_name_value = self.extra_options.get(INSTANCE_NAME_KEY, "")
        instance_name_value = (
            str(instance_name_value).strip() if instance_name_value else ""
        )

        # Determine if this is a named instance or LocalDB connection
        is_named_instance = (
            NAMED_INSTANCE_PATTERN.match(self.host) is not None
            or LOCALDB_PATTERN.match(self.host) is not None
            or instance_name_value
        )
        is_custom_port = self.port != DEFAULT_SQL_SERVER_PORT

        # Build server string with instance name if provided
        if instance_name_value:
            server = f"{self.host}\\{instance_name_value}"
        else:
            server = self.host

        # Select appropriate connection string template
        if self.use_windows_auth:
            if is_named_instance and not is_custom_port:
                template = SQLSERVER_CONNECTION_STRING_WIN_AUTH_NAMED_INSTANCE
            else:
                template = SQLSERVER_CONNECTION_STRING_WIN_AUTH
        else:
            if is_named_instance and not is_custom_port:
                template = SQLSERVER_CONNECTION_STRING_NAMED_INSTANCE
            else:
                template = SQLSERVER_CONNECTION_STRING

        # Build the connection string
        if self.use_windows_auth:
            connection_string = template.format(
                driver=self._resolved_odbc_driver,
                server=server,
                port=self.port,
                database=self.database,
            )
        else:
            connection_string = template.format(
                driver=self._resolved_odbc_driver,
                server=server,
                port=self.port,
                database=self.database,
                user=self.username or "",
                password=self.password or "",
            )

        # Add encryption settings if specified
        if self._encrypt_odbc_string is not None:
            connection_string += f";Encrypt={self._encrypt_odbc_string}"
        if self._trust_server_certificate_odbc_string is not None:
            connection_string += (
                f";TrustServerCertificate={self._trust_server_certificate_odbc_string}"
            )

        # Add extra options (excluding instance name and trusted connection)
        excluded_keys = {INSTANCE_NAME_KEY, TRUSTED_CONNECTION_KEY}
        for key, value in self.extra_options.items():
            stripped_key = str(key).strip()
            if stripped_key not in excluded_keys:
                stripped_value = str(value).strip()
                connection_string += f";{stripped_key}={stripped_value}"

        return connection_string

    @staticmethod
    def _bool_to_odbc_string(value: bool | None) -> str | None:
        """
        Convert a boolean value to ODBC connection string format.

        Args:
            value: Boolean value or None

        Returns:
            "yes" for True, "no" for False, None for None

        """
        if value is None:
            return None
        return "yes" if value else "no"

    def _validate_encryption_compatibility(self, encrypt_value: bool | None) -> None:
        """
        Validate encryption settings compatibility with the selected ODBC driver.

        Issues a warning if the user tries to enable encryption with ODBC Driver 17 or below,
        as these drivers don't support encryption the same way as Driver 18+.

        Args:
            encrypt_value: The original boolean value for encryption (before conversion to string)

        """
        # Only validate if encrypt is explicitly set to True
        if encrypt_value is not True:
            return

        # Extract driver version number
        version_match = re.search(
            r"Driver (\d+)", self._resolved_odbc_driver, re.IGNORECASE
        )
        if not version_match:
            # Not a versioned driver (e.g., SQL Server Native Client), skip validation
            return

        driver_version = int(version_match.group(1))

        # Warn if trying to enable encryption with Driver 17 or below
        if driver_version <= 17:
            logger.warning(
                f"Encryption is enabled (encrypt=True) but you are using '{self._resolved_odbc_driver}'. "
                f"Driver 17 and below have limited encryption support. If your SQL Server "
                f"does not have encryption configured, the connection may fail. "
                f"Consider setting encrypt=False or omitting the encrypt parameter entirely."
            )

    def _select_odbc_driver(
        self, requested_driver: str | None, auto_detect: bool
    ) -> str:
        """
        Select the appropriate ODBC driver with smart auto-detection.

        Args:
            requested_driver: The driver requested by the user (can be None)
            auto_detect: Whether to auto-detect if requested driver is not available

        Returns:
            str: The ODBC driver name to use

        Raises:
            ValueError: If no suitable driver can be found and auto_detect is True

        """
        # Case 1: Specific driver requested with auto_detect disabled
        # Just use it without validation - useful for testing and when you know the driver exists
        if requested_driver is not None and not auto_detect:
            logger.debug(
                f"Using explicitly specified ODBC driver without validation: {requested_driver}"
            )
            return requested_driver

        # Case 2: No driver specified - auto-detect the best one (only if auto_detect is True)
        if requested_driver is None:
            if auto_detect:
                best_driver = get_best_sqlserver_driver()
                if best_driver:
                    logger.info(
                        f"No ODBC driver specified. Auto-detected and using: {best_driver}"
                    )
                    return best_driver
                else:
                    self._raise_no_drivers_error()
            else:
                # No driver specified and auto_detect is False - use default
                logger.debug(
                    f"No ODBC driver specified and auto-detect disabled. Using default: {DEFAULT_ODBC_DRIVER}"
                )
                return DEFAULT_ODBC_DRIVER

        # Case 3: Specific driver requested with auto_detect enabled - validate it
        if is_driver_available(requested_driver):
            logger.info(f"Using requested ODBC driver: {requested_driver}")
            return requested_driver

        # Case 4: Requested driver not available but auto_detect is enabled
        # Try to find an alternative
        best_driver = get_best_sqlserver_driver()
        if best_driver:
            logger.warning(
                f"Requested ODBC driver '{requested_driver}' not found. "
                f"Auto-detected and using: {best_driver}"
            )
            return best_driver
        else:
            self._raise_no_drivers_error()

    def _raise_no_drivers_error(self) -> None:
        """Raise a helpful error when no SQL Server drivers are found."""
        raise ValueError(
            "No SQL Server ODBC drivers found on this system. "
            "Please install a SQL Server ODBC driver.\n\n"
            "For installation instructions, visit:\n"
            "https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server"
        )

    def _repr_fields(self) -> str:
        """Get string representation of fields (for use in __repr__)."""
        base_repr = super()._repr_fields()
        return f"use_windows_auth={self.use_windows_auth}, use_bcp={self.use_bcp}, {base_repr}"
