from __future__ import annotations

import json
import os
from dataclasses import dataclass
from pathlib import Path

from cli_demo import DEFAULT_MODEL, PACKAGE_NAME

API_KEY_ENV = "OPENAI_API_KEY"
BASE_URL_ENV = "OPENAI_BASE_URL"
MODEL_KEY = "OPENAI_MODEL"
ENV_KEY_MAP = {
    "OPENAI_API_KEY": "api_key",
    "OPENAI_BASE_URL": "base_url",
    "OPENAI_MODEL": "model",
    "MODEL": "model",
}


@dataclass(slots=True)
class AppConfig:
    api_key: str | None = None
    base_url: str | None = None
    model: str | None = None


class ConfigError(RuntimeError):
    pass


def get_config_path() -> Path:
    return Path.home() / ".cli-demo" / ".env"


def load_config() -> AppConfig:
    path = get_config_path()
    if not path.exists():
        return AppConfig()

    try:
        raw_text = path.read_text(encoding="utf-8")
    except OSError as exc:
        raise ConfigError(f"Could not read config file: {path}") from exc

    data: dict[str, str] = {}
    for line_number, raw_line in enumerate(raw_text.splitlines(), start=1):
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            raise ConfigError(f"Invalid .env line at {path}:{line_number}")

        key, raw_value = line.split("=", 1)
        normalized_key = key.strip()
        if normalized_key not in ENV_KEY_MAP:
            continue

        parsed_value = _parse_env_value(raw_value.strip(), path=path, line_number=line_number)
        data[ENV_KEY_MAP[normalized_key]] = parsed_value

    return AppConfig(
        api_key=_load_optional_field(data, "api_key"),
        base_url=_load_optional_field(data, "base_url"),
        model=_load_optional_field(data, "model"),
    )


def save_config(config: AppConfig) -> Path:
    path = get_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)

    values: list[tuple[str, str | None]] = [
        ("OPENAI_API_KEY", config.api_key),
        ("OPENAI_BASE_URL", config.base_url),
        (MODEL_KEY, config.model),
    ]
    lines = [f"{key}={json.dumps(value)}" for key, value in values if value is not None]

    try:
        if lines:
            path.write_text("\n".join(lines) + "\n", encoding="utf-8")
        elif path.exists():
            path.unlink()
    except OSError as exc:
        raise ConfigError(f"Could not write config file: {path}") from exc

    return path


def get_effective_api_key(config: AppConfig) -> str | None:
    return config.api_key or normalize_optional_value(os.getenv(API_KEY_ENV), field_name=API_KEY_ENV)


def get_effective_base_url(config: AppConfig) -> str | None:
    return config.base_url or normalize_optional_value(os.getenv(BASE_URL_ENV), field_name=BASE_URL_ENV)


def get_effective_model(config: AppConfig, *, override: str | None = None) -> str:
    return (
        normalize_optional_value(override, field_name="model")
        or config.model
        or DEFAULT_MODEL
    )


def normalize_optional_value(value: str | None, *, field_name: str) -> str | None:
    if value is None:
        return None
    normalized = " ".join(value.split())
    if not normalized:
        raise ConfigError(f"{field_name} cannot be empty or whitespace.")
    return normalized


def mask_secret(value: str | None) -> str:
    if not value:
        return "<not set>"
    if len(value) <= 8:
        return "*" * len(value)
    return f"{value[:4]}...{value[-4:]}"


def _load_optional_field(data: dict[str, object], key: str) -> str | None:
    value = data.get(key)
    if value is None:
        return None
    if not isinstance(value, str):
        raise ConfigError(f"{key} in config must be a string.")
    return normalize_optional_value(value, field_name=key)


def _parse_env_value(raw_value: str, *, path: Path, line_number: int) -> str:
    if not raw_value:
        raise ConfigError(f"Config value cannot be empty at {path}:{line_number}")

    if raw_value.startswith('"'):
        try:
            value = json.loads(raw_value)
        except json.JSONDecodeError as exc:
            raise ConfigError(f"Invalid quoted value at {path}:{line_number}") from exc
        if not isinstance(value, str):
            raise ConfigError(f"Config value must be a string at {path}:{line_number}")
        return value

    return raw_value
