from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
import logging
from pathlib import Path
import tomllib
from urllib.parse import urlparse

logger = logging.getLogger(__name__)
DEFAULT_SETTINGS_PATH = Path("settings.toml")


@dataclass(frozen=True, slots=True)
class InfluxSettings:
    """InfluxDB connection settings.

    Attributes
    ----------
    url : str
        Base URL for the InfluxDB server.
    token : str
        Authentication token.
    org : str
        InfluxDB organization name.
    bucket : str
        Target bucket name.
    default_tags : dict[str, str]
        Tags automatically attached to each written point.
    """

    url: str
    token: str
    org: str
    bucket: str
    default_tags: dict[str, str] = field(default_factory=dict)


def _required_string(data: dict[str, object], key: str) -> str:
    """Return a required non-empty string setting.

    Parameters
    ----------
    data : dict[str, object]
        Parsed TOML dictionary.
    key : str
        Key to read.

    Returns
    -------
    str
        Stripped string value.

    Raises
    ------
    ValueError
        If the key is missing or not a non-empty string.
    """
    value = data.get(key)
    if not isinstance(value, str) or not value.strip():
        logger.error("Invalid settings value for '%s': expected non-empty string.", key)
        raise ValueError(f"'{key}' must be a non-empty string.")
    return value.strip()


def _escape_toml_string(value: str) -> str:
    """Escape one TOML basic string value.

    Parameters
    ----------
    value : str
        Raw string value.

    Returns
    -------
    str
        Escaped string safe for TOML basic string syntax.
    """
    return (
        value.replace("\\", "\\\\")
        .replace('"', '\\"')
        .replace("\n", "\\n")
        .replace("\r", "\\r")
        .replace("\t", "\\t")
    )


def _validate_url(url: str) -> None:
    """Validate that a URL is HTTP or HTTPS.

    Parameters
    ----------
    url : str
        URL value to validate.

    Raises
    ------
    ValueError
        If URL is not a valid HTTP or HTTPS URL.
    """
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        logger.error("Invalid InfluxDB URL format provided.")
        raise ValueError("'url' must be a valid http/https URL.")


def write_settings_file(
    *,
    url: str,
    org: str,
    bucket: str,
    token: str,
    path: str | Path = DEFAULT_SETTINGS_PATH,
    default_tags: Mapping[str, str] | None = None,
    overwrite: bool = False,
) -> Path:
    """Write a settings TOML file for DeteQT.

    Parameters
    ----------
    url : str
        InfluxDB HTTP(S) base URL.
    org : str
        InfluxDB organization.
    bucket : str
        InfluxDB bucket.
    token : str
        InfluxDB token (typically write token for users).
    path : str | Path, default=\"settings.toml\"
        Target TOML path to create.
    default_tags : Mapping[str, str] | None, default=None
        Optional default tags written under ``[defaults.tags]``.
    overwrite : bool, default=False
        If ``False`` and target already exists, raise ``FileExistsError``.

    Returns
    -------
    Path
        Path to the written settings file.

    Raises
    ------
    FileExistsError
        If target exists and ``overwrite`` is ``False``.
    ValueError
        If required values or ``default_tags`` are invalid.
    """
    clean_url = _required_string({"url": url}, "url")
    _validate_url(clean_url)
    clean_org = _required_string({"org": org}, "org")
    clean_bucket = _required_string({"bucket": bucket}, "bucket")
    clean_token = _required_string({"token": token}, "token")

    normalized_tags: dict[str, str] = {}
    if default_tags is not None:
        for raw_key, raw_value in default_tags.items():
            if not isinstance(raw_key, str) or not raw_key.strip():
                raise ValueError("Each default tag key must be a non-empty string.")
            if not isinstance(raw_value, str) or not raw_value.strip():
                raise ValueError(
                    f"Default tag '{raw_key}' must be a non-empty string value."
                )
            clean_key = raw_key.strip()
            clean_value = raw_value.strip()
            normalized_tags[clean_key] = clean_value

    settings_path = Path(path).expanduser()
    if settings_path.exists() and not overwrite:
        raise FileExistsError(
            f"Settings file already exists: {settings_path}. "
            "Set overwrite=True to replace it."
        )

    if settings_path.parent != Path("."):
        settings_path.parent.mkdir(parents=True, exist_ok=True)

    lines = [
        f'url = "{_escape_toml_string(clean_url)}"',
        f'org = "{_escape_toml_string(clean_org)}"',
        f'bucket = "{_escape_toml_string(clean_bucket)}"',
        f'token = "{_escape_toml_string(clean_token)}"',
    ]

    if normalized_tags:
        lines.extend(["", "[defaults]", "[defaults.tags]"])
        for key, value in sorted(normalized_tags.items()):
            lines.append(
                f'"{_escape_toml_string(key)}" = "{_escape_toml_string(value)}"'
            )

    content = "\n".join(lines) + "\n"
    settings_path.write_text(content, encoding="utf-8")
    return settings_path


def load_settings(path: str | Path | None = None) -> InfluxSettings:
    """Load and validate InfluxDB settings from TOML.

    Parameters
    ----------
    path : str | Path | None, default=None
        Path to a TOML settings file. If ``None``, uses ``settings.toml`` in
        the current working directory.

    Returns
    -------
    InfluxSettings
        Validated settings object.

    Raises
    ------
    FileNotFoundError
        If the settings file does not exist.
    ValueError
        If required keys are missing or contain invalid values.
    """

    settings_path = Path(path) if path is not None else DEFAULT_SETTINGS_PATH
    logger.debug("Loading InfluxDB settings from '%s'.", settings_path)

    if not settings_path.exists():
        logger.error("Settings file not found at '%s'.", settings_path)
        raise FileNotFoundError(
            f"Settings file not found: {settings_path}. "
            "Create settings.toml in your current directory or pass "
            "settings_path=... with url/org/bucket/token."
        )

    with settings_path.open("rb") as file:
        data = tomllib.load(file)
    logger.debug("Parsed settings TOML successfully.")

    url = _required_string(data, "url")
    _validate_url(url)

    token = _required_string(data, "token")
    org = _required_string(data, "org")
    bucket = _required_string(data, "bucket")

    defaults = data.get("defaults", {})
    if not isinstance(defaults, dict):
        logger.error("Invalid settings: 'defaults' is not a TOML table.")
        raise ValueError("'defaults' must be a table.")

    raw_tags = defaults.get("tags", {})
    if not isinstance(raw_tags, dict):
        logger.error("Invalid settings: 'defaults.tags' is not a TOML table.")
        raise ValueError("'defaults.tags' must be a table.")

    default_tags: dict[str, str] = {}
    for key, value in raw_tags.items():
        if not isinstance(key, str) or not key.strip():
            logger.error("Invalid default tag key in settings.")
            raise ValueError("Each default tag key must be a non-empty string.")
        if not isinstance(value, str) or not value.strip():
            logger.error("Invalid default tag value for key '%s'.", key)
            raise ValueError(f"Default tag '{key}' must be a non-empty string value.")
        default_tags[key.strip()] = value.strip()

    settings = InfluxSettings(
        url=url,
        token=token,
        org=org,
        bucket=bucket,
        default_tags=default_tags,
    )
    logger.info(
        "Loaded InfluxDB settings for org='%s', bucket='%s'.",
        settings.org,
        settings.bucket,
    )
    logger.debug("Default tags configured: %d", len(settings.default_tags))
    return settings
