#!/usr/bin/env python3
"""
Shared utilities for deployment scripts.
"""

import hashlib
import os
from pathlib import Path

import yaml
from cognite.client import ClientConfig, CogniteClient
from cognite.client.credentials import OAuthClientCredentials


def _deep_merge_dicts(base: dict, override: dict) -> dict:
    """Recursively merge two dictionaries."""
    merged = dict(base)
    for key, value in (override or {}).items():
        if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
            merged[key] = _deep_merge_dicts(merged[key], value)
        else:
            merged[key] = value
    return merged


def _get_config_env(config_env: str | None = None) -> str | None:
    """Return CONFIG_ENV value if provided or set in environment."""
    config_env = (config_env or os.environ.get("CONFIG_ENV") or "").strip()
    return config_env or None


def get_repo_root() -> Path:
    """Get the repository root directory."""
    return Path(__file__).parent.parent.resolve()


def load_settings(repo_root: Path | None = None, config_env: str | None = None) -> dict:
    """Load settings for the given CONFIG_ENV (env file can stand alone)."""
    if repo_root is None:
        repo_root = get_repo_root()

    config_env = _get_config_env(config_env)
    base_settings: dict = {}

    # Base settings are optional; env settings may stand alone
    settings_path = repo_root / "config" / "settings.yaml"
    if settings_path.exists():
        with open(settings_path) as f:
            base_settings = yaml.safe_load(f) or {}

    env_settings_path = repo_root / "config" / "environments" / config_env / "settings.yaml" if config_env else None
    if env_settings_path and env_settings_path.exists():
        with open(env_settings_path) as f:
            env_settings = yaml.safe_load(f) or {}
        base_settings = _deep_merge_dicts(base_settings, env_settings)
    elif not settings_path.exists():
        raise FileNotFoundError(f"No settings found for CONFIG_ENV='{config_env}' and base settings.yaml is missing")

    base_settings["_config_env"] = config_env
    return base_settings


def load_view_configs(repo_root: Path | None = None, config_env: str | None = None) -> list[dict]:
    """Load all view configurations, honoring CONFIG_ENV overrides."""
    if repo_root is None:
        repo_root = get_repo_root()

    config_env = _get_config_env(config_env)
    base_views_dir = repo_root / "config" / "views"
    env_views_dir = repo_root / "config" / "environments" / config_env / "views" if config_env else None
    views_dir = env_views_dir if env_views_dir and env_views_dir.exists() else base_views_dir
    if not views_dir.exists():
        raise FileNotFoundError(f"Views directory not found: {views_dir}")

    configs = []
    for config_file in sorted(views_dir.glob("*.yaml")):
        with open(config_file) as f:
            config = yaml.safe_load(f)
            config["_filename"] = config_file.name
            configs.append(config)

    return configs


def load_timeseries_configs(
    repo_root: Path | None = None, settings: dict | None = None, config_env: str | None = None
) -> list[dict]:
    """Load all time series validation configurations with env-aware settings."""
    if repo_root is None:
        repo_root = get_repo_root()

    if settings is None:
        settings = load_settings(repo_root, config_env)

    config_dir = repo_root / settings.get("timeseries", {}).get("config_dir", "config/timeseries")

    if not config_dir.exists():
        return []

    configs = []
    for config_file in sorted(config_dir.glob("*.yaml")):
        with open(config_file) as f:
            config = yaml.safe_load(f)
            config["_filename"] = config_file.name
            configs.append(config)

    return configs


def create_client() -> CogniteClient:
    """Create a Cognite client from environment variables."""
    cluster = os.environ.get("COGNITE_CLUSTER", "api")
    project = os.environ.get("COGNITE_PROJECT")
    client_id = os.environ.get("COGNITE_CLIENT_ID")
    client_secret = os.environ.get("COGNITE_CLIENT_SECRET")
    # Use 'or' to handle empty string from unset GitHub variable
    base_url = os.environ.get("COGNITE_BASE_URL") or f"https://{cluster}.cognitedata.com"

    # Support both token URL and tenant ID
    token_url = os.environ.get("COGNITE_TOKEN_URL")
    tenant_id = os.environ.get("AZURE_TENANT_ID")

    if not all([project, client_id, client_secret]):
        raise ValueError(
            "Missing required environment variables: COGNITE_PROJECT, COGNITE_CLIENT_ID, COGNITE_CLIENT_SECRET"
        )

    if token_url:
        oauth_url = token_url
    elif tenant_id:
        oauth_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    else:
        raise ValueError("Missing COGNITE_TOKEN_URL or AZURE_TENANT_ID")

    scopes = [f"https://{cluster}.cognitedata.com/.default"]

    credentials = OAuthClientCredentials(
        token_url=oauth_url, client_id=client_id, client_secret=client_secret, scopes=scopes
    )

    config = ClientConfig(
        client_name="dq-validation-deploy", project=project, credentials=credentials, base_url=base_url
    )

    return CogniteClient(config)


def compute_function_hash(repo_root: Path, settings: dict) -> str:
    """
    Compute a hash of the function code and dependencies.
    Used to determine if the function needs redeployment.
    """
    source_dir = repo_root / settings["function"]["source_dir"]
    hasher = hashlib.sha256()

    # Hash all included files
    for filename in settings["function"]["include_files"]:
        file_path = source_dir / filename
        if file_path.exists():
            hasher.update(file_path.read_bytes())

    # Hash wheel files
    for whl in source_dir.glob("*.whl"):
        hasher.update(whl.read_bytes())

    return hasher.hexdigest()[:16]


def compute_view_config_hash(repo_root: Path, settings: dict, view_config: dict) -> str:
    """
    Compute a hash of the view configuration including SHACL rules content.
    Used to determine if workflow/trigger needs redeployment.
    """
    hasher = hashlib.sha256()

    # Hash the view config (excluding internal fields)
    config_copy = {k: v for k, v in view_config.items() if not k.startswith("_")}
    hasher.update(yaml.dump(config_copy, sort_keys=True).encode())

    # Hash relevant settings
    relevant_settings = {
        "workflow": settings.get("workflow", {}),
        "trigger": settings.get("trigger", {}),
        "records": settings.get("records", {}),
        "function": {"external_id": settings.get("function", {}).get("external_id")},
    }
    hasher.update(yaml.dump(relevant_settings, sort_keys=True).encode())

    # Hash SHACL rules file content
    shacl_dir = repo_root / settings["shacl_rules"]["source_dir"]
    shacl_file = shacl_dir / view_config["shacl_rules"]["file"]
    if shacl_file.exists():
        hasher.update(shacl_file.read_bytes())

    return hasher.hexdigest()[:16]
