"""
Configuration loader for ``settings/test_config.yaml``.

Handles environment-variable expansion (``${VAR}`` syntax) and provides
typed accessors for connection settings.

The YAML layout contains project-level configuration::

    source_platform: sqlserver
    target_platform: snowflake
    output_directory_path: ./reports

    validation_configuration:
      schema_validation: true
      metrics_validation: true
      row_validation: false
      max_failed_rows_number: 100

    # test-runner extension
    selectors:
      schemas: [DBO]

Connection details are resolved at runtime from TOML files or CLI flags:

- **Source**: ``~/.snowflake/snowct/<dialect>.toml`` (default_connection_name)
  or ``--source-connection`` CLI flag.
- **Target** (Snowflake): ``~/.snowflake/config.toml`` (default_connection_name)
  or ``--connection`` CLI flag.

Connection details are kept as raw dicts so that each dialect's
:class:`DatabaseExecutorFactory` can parse them into its own typed config
(e.g. ``SqlServerCredentialsConnection``, ``SnowflakeNamedConnection``).
"""

from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional

import yaml

from .connection_reader import (
    resolve_default_source_connection,
    resolve_default_target_connection,
    resolve_named_connection,
)
from .database_dialect import DatabaseDialect, parse_dialect


# ---------------------------------------------------------------------------
# Public data classes
# ---------------------------------------------------------------------------

@dataclass
class SelectorsConfig:
    """Optional selectors to filter which test files are processed."""

    schemas: list[str] = field(default_factory=list)
    exclude: list[str] = field(default_factory=list)


@dataclass
class ValidationConfiguration:
    """Validation level toggles, row limits, and in-memory comparison settings."""

    schema_validation: bool = True
    """Run schema-level validation (column names and type mapping)."""

    metrics_validation: bool = True
    """Run metrics-level validation (per-column aggregate metrics)."""

    row_validation: bool = True
    """Run row-level validation (row-by-row MD5 comparison)."""

    max_failed_rows_number: int = 100
    """Maximum number of failed rows to report before stopping."""

    in_memory_row_threshold: int = 10_000
    """Use in-memory (DuckDB) comparison when baseline row count <= this value.
    Set to 0 to always use the transient-table path."""

    tolerance: float = 0.001
    """Two numeric values are considered equal when ``abs(a - b) <= tolerance``."""

    max_cell_diffs: int = 1_000
    """Maximum number of differing cells returned per in-memory validation run."""


@dataclass
class ReplayConfiguration:
    """Settings for side-effect (DML) replay validation.

    Controls how the replay framework isolates environments, selects
    delta-capture strategies, and compares table mutations produced by
    procedures classified as ``modifies_data=True``.
    """

    small_table_threshold: int = 100_000
    """Tables with fewer rows use full-snapshot diff (no PK needed)."""

    large_table_threshold: int = 10_000_000
    """Tables above this size require PK-based or synthetic-key diff."""

    delta_ignore_columns: list[str] = field(default_factory=list)
    """Column names excluded from delta comparison (case-insensitive).

    Useful for platform-managed columns that differ by design, e.g.
    SQL Server temporal columns ``ValidFrom`` / ``ValidTo`` that have
    no Snowflake equivalent.
    """


@dataclass
class TestRunnerConfig:
    """Top-level configuration for capture / validate runs.

    Connection details are raw dicts -- each dialect's factory parses them.
    """

    source_dialect: DatabaseDialect = DatabaseDialect.SQL_SERVER
    """Source database dialect (read from ``source_platform``)."""

    target_platform: str = "snowflake"
    """Target platform identifier (read from ``target_platform``)."""

    source_connection_raw: dict[str, Any] = field(default_factory=dict)
    """Raw ``source_connection`` dict from YAML."""

    target_connection_raw: dict[str, Any] = field(default_factory=dict)
    """Raw ``target_connection`` dict from YAML."""

    output_directory_path: Optional[str] = None
    """Directory for validation reports (``output_directory_path``)."""

    validation_configuration: Optional[ValidationConfiguration] = None
    """Validation level toggles and limits."""

    selectors: Optional[SelectorsConfig] = None
    """Test-runner extension: filter which test files are processed."""

    validation_database: str = ""
    """Snowflake database for validation objects (defaults to project folder name, uppercased)."""

    replay_configuration: ReplayConfiguration = field(default_factory=ReplayConfiguration)
    """Side-effect replay settings for DML procedures (always present with defaults)."""

    raw: dict[str, Any] = field(default_factory=dict)
    """The full raw parsed YAML dict."""


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ENV_VAR_PATTERN = re.compile(r"\$\{(\w+)\}")


def _expand_env_vars(value: Any) -> Any:
    """Recursively expand ``${VAR}`` references in strings, dicts and lists."""
    if isinstance(value, str):
        return _ENV_VAR_PATTERN.sub(
            lambda m: os.environ.get(m.group(1), m.group(0)), value
        )
    if isinstance(value, dict):
        return {k: _expand_env_vars(v) for k, v in value.items()}
    if isinstance(value, list):
        return [_expand_env_vars(v) for v in value]
    return value


def _build_selectors(raw: dict[str, Any] | None) -> Optional[SelectorsConfig]:
    if not raw:
        return None
    return SelectorsConfig(
        schemas=[s.upper() for s in raw.get("schemas", [])],
        exclude=raw.get("exclude", []),
    )


def _build_validation_configuration(
    raw: dict[str, Any] | None,
) -> Optional[ValidationConfiguration]:
    if not raw:
        return None
    return ValidationConfiguration(
        schema_validation=bool(raw.get("schema_validation", True)),
        metrics_validation=bool(raw.get("metrics_validation", True)),
        row_validation=bool(raw.get("row_validation", True)),
        max_failed_rows_number=int(raw.get("max_failed_rows_number", 100)),
        in_memory_row_threshold=int(raw.get("in_memory_row_threshold", 10_000)),
        tolerance=float(raw.get("tolerance", 0.001)),
        max_cell_diffs=int(raw.get("max_cell_diffs", 1_000)),
    )


def _build_replay_configuration(
    raw: dict[str, Any] | None,
) -> ReplayConfiguration:
    if not raw:
        return ReplayConfiguration()
    return ReplayConfiguration(
        small_table_threshold=int(raw.get("small_table_threshold", 100_000)),
        large_table_threshold=int(raw.get("large_table_threshold", 10_000_000)),
        delta_ignore_columns=list(raw.get("delta_ignore_columns", [])),
    )


_DIALECT_TOML_FILES: dict[DatabaseDialect, str] = {
    DatabaseDialect.SQL_SERVER: "sqlserver.toml",
    DatabaseDialect.ORACLE: "oracle.toml",
    DatabaseDialect.TERADATA: "teradata.toml",
    DatabaseDialect.POSTGRESQL: "postgresql.toml",
    DatabaseDialect.REDSHIFT: "redshift.toml",
}


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def load_config(
    project_root: str | Path,
    *,
    source_connection: str | None = None,
    target_connection: str | None = None,
) -> TestRunnerConfig:
    """Load ``settings/test_config.yaml`` from *project_root*.

    Connection details are **not** read from the YAML.  They are resolved
    from TOML defaults or CLI overrides:

    *source_connection*
        Named source connection (``--source-connection`` CLI flag).  When
        provided, resolved from ``.snowflake/snowct/<dialect>.toml``.
        When ``None``, the ``default_connection_name`` from the TOML is used.

    *target_connection*
        Named Snowflake target connection (``--connection`` CLI flag).  When
        provided, resolved from ``~/.snowflake/config.toml``.
        When ``None``, the ``default_connection_name`` from the TOML is used.

    Raises ``FileNotFoundError`` if the config YAML is missing.
    """
    config_path = Path(project_root) / "settings" / "test_config.yaml"
    if not config_path.exists():
        raise FileNotFoundError(
            f"Config file not found: {config_path}\n"
            "Create settings/test_config.yaml with platform and validation settings."
        )

    with open(config_path, "r", encoding="utf-8") as fh:
        raw: dict[str, Any] = yaml.safe_load(fh) or {}

    # -- platforms -----------------------------------------------------------
    source_platform_str = raw.get("source_platform", "sqlserver")
    source_dialect = parse_dialect(source_platform_str)
    target_platform = raw.get("target_platform", "snowflake")

    # -- source connection (from CLI flag or TOML default) -------------------
    dialect_file = _DIALECT_TOML_FILES.get(
        source_dialect, f"{source_dialect.value}.toml",
    )
    if source_connection:
        source_raw: dict[str, Any] = resolve_named_connection(
            source_connection, dialect_file, project_root,
        )
    else:
        try:
            source_raw = resolve_default_source_connection(
                dialect_file, project_root,
            )
        except (FileNotFoundError, ValueError, KeyError):
            source_raw = {}

    # -- target connection (from CLI flag or TOML default) -------------------
    if target_connection:
        target_raw: dict[str, Any] = {"mode": "name", "name": target_connection}
    else:
        try:
            target_raw = resolve_default_target_connection(project_root)
            target_raw.setdefault("mode", "name")
        except (FileNotFoundError, ValueError, KeyError):
            target_raw = {}

    # -- validation database (defaults to project folder name) ----------------
    validation_database = raw.get("validation_database", "")
    if not validation_database:
        validation_database = Path(project_root).resolve().name.upper()

    # -- build config --------------------------------------------------------
    return TestRunnerConfig(
        source_dialect=source_dialect,
        target_platform=target_platform,
        source_connection_raw=source_raw,
        target_connection_raw=target_raw,
        output_directory_path=raw.get("output_directory_path"),
        validation_configuration=_build_validation_configuration(
            raw.get("validation_configuration"),
        ),
        selectors=_build_selectors(raw.get("selectors")),
        validation_database=validation_database,
        replay_configuration=_build_replay_configuration(
            raw.get("replay_configuration"),
        ),
        raw=raw,
    )
