import json
import os
from typing import Any, Dict, Iterable, Optional


class VaultClient:
    def __init__(self):
        pass

    def get_secret(self, conn_id: str) -> Optional[str]:
        return None


def _airflow_connection_as_json(conn_name: str) -> str:
    try:
        from airflow.hooks.base import BaseHook
    except ImportError:
        from airflow.hooks.base_hook import BaseHook

    conn = BaseHook.get_connection(conn_name)
    ci = {
        "host": conn.host,
        "password": conn.password,
        "login": conn.login,
        "port": conn.port,
        "schema": conn.schema,
        "extra": conn.extra,
    }
    return json.dumps(ci)


def get_connection_as_json(conn_name: str) -> str:
    secret = os.environ.get(conn_name)
    if secret:
        return secret

    try:
        vault_client = VaultClient()
        secret = vault_client.get_secret(conn_name)
        if secret:
            return secret
    except Exception:
        pass

    try:
        return _airflow_connection_as_json(conn_name)
    except ImportError:
        pass
    except Exception:
        pass

    raise ValueError(
        f"Секрет {conn_name} не найден в переменных окружения, Vault или Airflow"
    )


def get_secret(conn_id: str) -> Dict:
    raw = get_connection_as_json(conn_id)
    try:
        return json.loads(raw)
    except json.JSONDecodeError as e:
        raise ValueError(f"Секрет {conn_id} не является корректным JSON: {e}")


def _get_test_secret_candidates(
    conn_id: str,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> list[str]:
    filename = f"{conn_id}.json"
    candidates: list[str] = []

    if creds_path:
        abs_creds_path = os.path.abspath(creds_path)
        if os.path.isdir(abs_creds_path):
            candidates.append(os.path.join(abs_creds_path, filename))
        else:
            candidates.append(abs_creds_path)

    if dir_path:
        abs_dir_path = os.path.abspath(dir_path)
        if os.path.isdir(abs_dir_path):
            candidates.append(os.path.join(abs_dir_path, filename))
        else:
            candidates.append(abs_dir_path)

    candidates.append(os.path.join(os.getcwd(), filename))
    return candidates


def get_connection_as_json_test(
    conn_id: str,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> str:
    candidates = _get_test_secret_candidates(
        conn_id=conn_id,
        dir_path=dir_path,
        creds_path=creds_path,
    )

    for file_path in candidates:
        if os.path.isfile(file_path):
            with open(file_path, "r", encoding="utf-8") as f:
                return f.read()

    raise FileNotFoundError(
        f"Тестовый секрет {conn_id}.json не найден. Проверены пути: {candidates}"
    )


def get_secret_test(
    conn_id: str,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict:
    if creds_path is None:
        raw = get_connection_as_json_test(conn_id, dir_path=dir_path)
    else:
        raw = get_connection_as_json_test(
            conn_id,
            dir_path=dir_path,
            creds_path=creds_path,
        )
    try:
        return json.loads(raw)
    except json.JSONDecodeError as e:
        raise ValueError(f"Тестовый секрет {conn_id} не является корректным JSON: {e}")


def _get_secret_test_compat(
    conn_id: str,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict:
    if creds_path is None:
        return get_secret_test(conn_id, dir_path=dir_path)
    return get_secret_test(conn_id, dir_path=dir_path, creds_path=creds_path)


def has_connection(conn_name: str) -> bool:
    try:
        get_connection_as_json(conn_name)
        return True
    except Exception:
        return False


def _deep_get(data: Dict[str, Any], field_path: str) -> Any:
    if not field_path:
        return data
    current: Any = data
    for part in field_path.split("."):
        if isinstance(current, dict) and part in current:
            current = current[part]
        else:
            raise KeyError(field_path)
    return current


def get_secret_field(
    conn_id: str,
    field_path: str,
    default: Any = None,
    *,
    required: bool = False,
    test: bool = False,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Any:
    secret = (
        _get_secret_test_compat(
            conn_id=conn_id,
            dir_path=dir_path,
            creds_path=creds_path,
        )
        if test
        else get_secret(conn_id)
    )
    try:
        value = _deep_get(secret, field_path)
    except KeyError:
        if required:
            raise KeyError(f"Поле '{field_path}' отсутствует в секрете '{conn_id}'")
        return default

    if value is None and required:
        raise ValueError(f"Поле '{field_path}' в секрете '{conn_id}' пустое")
    return value


def require_secret_fields(
    conn_id: str,
    required_fields: Iterable[str],
    *,
    test: bool = False,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    secret = (
        _get_secret_test_compat(
            conn_id=conn_id,
            dir_path=dir_path,
            creds_path=creds_path,
        )
        if test
        else get_secret(conn_id)
    )
    missing = []

    for field in required_fields:
        try:
            value = _deep_get(secret, field)
        except KeyError:
            missing.append(field)
            continue
        if value is None or (isinstance(value, str) and not value.strip()):
            missing.append(field)

    if missing:
        raise ValueError(
            f"В секрете '{conn_id}' отсутствуют обязательные поля: {', '.join(missing)}"
        )
    return secret


def get_secret_with_defaults(
    conn_id: str,
    defaults: Dict[str, Any],
    *,
    test: bool = False,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    secret = (
        _get_secret_test_compat(
            conn_id=conn_id,
            dir_path=dir_path,
            creds_path=creds_path,
        )
        if test
        else get_secret(conn_id)
    )
    out = dict(defaults)
    out.update(secret)
    return out
