"""設定ファイル（TOML）の読み込みと検証.

データの場所（目録・ストア・取り込み元・計算サーバ）はコードに書かず，
研究室 PC のユーザー領域に置いた設定ファイルから受け取る（SPEC_01「設定ファイル」）．
"""

import os
import tomllib
from pathlib import Path

from pydantic import BaseModel, ConfigDict, ValidationInfo, field_validator, model_validator

CONFIG_ENV_VAR = "TIASDATA_CONFIG"

CATALOG_FILENAME = "catalog.sqlite"


def default_config_path() -> Path:
    """設定ファイルの場所を返す．

    環境変数 `TIASDATA_CONFIG` があればそのパス，無ければ `~/.config/tiasdata/config.toml`
    （Windows でも同じ相対位置．`%USERPROFILE%\\.config\\tiasdata\\config.toml`）．

    Returns:
        設定ファイルのパス（存在するかは確かめない）．
    """
    env = os.environ.get(CONFIG_ENV_VAR)
    if env:
        return Path(env).expanduser()
    return Path.home() / ".config" / "tiasdata" / "config.toml"


def _resolve_path(value: Path, info: ValidationInfo) -> Path:
    # 相対パスは設定ファイルのあるフォルダを基準にする（作業フォルダに依存させないため）
    path = value.expanduser()
    base_dir = info.context.get("base_dir") if info.context else None
    if base_dir is not None and not path.is_absolute():
        path = Path(base_dir) / path
    return path


class _StrictModel(BaseModel):
    # 未知のキーは拒否する（綴りの誤りで設定が黙って無視されるのを防ぐ）
    model_config = ConfigDict(extra="forbid", frozen=True)


class CatalogConfig(_StrictModel):
    """`[catalog]`: 目録・ストア・対応表の場所．

    Attributes:
        root: `catalog.sqlite`・`store/`・`bundles/`・`backups/` を置くフォルダ．
        identity: 対応表（`identity.sqlite`）のパス．`root` とは別のフォルダに置く．
    """

    root: Path
    identity: Path

    @field_validator("root", "identity")
    @classmethod
    def _resolve(cls, value: Path, info: ValidationInfo) -> Path:
        return _resolve_path(value, info)

    @model_validator(mode="after")
    def _identity_outside_root(self) -> "CatalogConfig":
        # 対応表は目録とは別のフォルダに置く（SPEC_01「構成要素」）．
        # 目録のバックアップ・受け渡しに対応表が紛れ込まないようにするため
        root = self.root.resolve()
        identity_dir = self.identity.resolve().parent
        if identity_dir == root or root in identity_dir.parents:
            raise ValueError("catalog.identity は catalog.root の外のフォルダに置く")
        return self

    @property
    def catalog_path(self) -> Path:
        """目録（`catalog.sqlite`）のパス．"""
        return self.root / CATALOG_FILENAME

    @property
    def store_dir(self) -> Path:
        """ストアのフォルダ．"""
        return self.root / "store"

    @property
    def bundles_dir(self) -> Path:
        """書き出したバンドルのフォルダ．"""
        return self.root / "bundles"

    @property
    def backups_dir(self) -> Path:
        """目録のバックアップのフォルダ．"""
        return self.root / "backups"


class SourceConfig(_StrictModel):
    """`[sources.<名前>]`: 取り込み元．

    Attributes:
        path: 取り込み元のフォルダ（読むだけ）．
        importer: 取り込み元の形（`importers/` のモジュール名．例: `tongue_color_folders`）．
        patient_map: 画像の番号 → 患者 ID の対応表のファイル（任意）．
    """

    path: Path
    importer: str
    patient_map: Path | None = None

    @field_validator("path", "patient_map")
    @classmethod
    def _resolve(cls, value: Path | None, info: ValidationInfo) -> Path | None:
        if value is None:
            return None
        return _resolve_path(value, info)


class ServerConfig(_StrictModel):
    """`[servers.<名前>]`: 計算サーバ（受け渡し先）．

    Attributes:
        host: SSH の接続先（`user@host`）．
        key: SSH の秘密鍵のパス（研究室 PC 上）．
        root: サーバ上のバンドルの置き場所．サーバ側で解釈するため `~` は展開しない．
    """

    host: str
    key: Path
    root: str

    @field_validator("key")
    @classmethod
    def _resolve(cls, value: Path, info: ValidationInfo) -> Path:
        return _resolve_path(value, info)


class Config(_StrictModel):
    """設定ファイル全体．

    Attributes:
        catalog: 目録・ストア・対応表の場所．
        sources: 取り込み元（名前 → 設定）．
        servers: 計算サーバ（名前 → 設定）．
    """

    catalog: CatalogConfig
    sources: dict[str, SourceConfig] = {}
    servers: dict[str, ServerConfig] = {}


def load_config(path: Path | None = None) -> Config:
    """設定ファイルを読み込んで検証する．

    Args:
        path: 設定ファイルのパス．省略時は `default_config_path()`．

    Returns:
        検証済みの設定．相対パスは設定ファイルのあるフォルダ基準の絶対パスに，`~` は展開済み．

    Raises:
        ValueError: 設定ファイルが無い，または TOML として読めない．
        pydantic.ValidationError: 未知のキー・必須キーの欠落・値の形の誤り．
    """
    config_path = path if path is not None else default_config_path()
    try:
        raw = config_path.read_bytes()
    except FileNotFoundError:
        raise ValueError(
            f"設定ファイルが見つからない: {config_path}"
            f"（環境変数 {CONFIG_ENV_VAR} で場所を指定できる）"
        ) from None
    try:
        data = tomllib.loads(raw.decode("utf-8"))
    except (UnicodeDecodeError, tomllib.TOMLDecodeError) as e:
        raise ValueError(f"設定ファイルを TOML として読めない: {e}") from e
    base_dir = config_path.expanduser().resolve().parent
    return Config.model_validate(data, context={"base_dir": base_dir})
