"""Resolution service with per-process caching."""

from __future__ import annotations

from threading import Lock
from typing import Any

from simorgh.apps.app_settings.models import SettingScope, SettingValue
from simorgh.apps.app_settings.registry import SettingsError, get_definition

# Cache key → resolved value. Cleared on any write via `invalidate()`.
_CACHE: dict[tuple, Any] = {}
_CACHE_LOCK = Lock()


def _cache_key(key: str, tenant_id, org_node_id, user_id, module) -> tuple:
    return (key, tenant_id, org_node_id, user_id, module)


def invalidate(key: str | None = None) -> None:
    with _CACHE_LOCK:
        if key is None:
            _CACHE.clear()
        else:
            for k in list(_CACHE):
                if k[0] == key:
                    _CACHE.pop(k, None)


def resolve(
    key: str,
    *,
    tenant_id: int | None = None,
    org_node_id: int | None = None,
    user_id: int | None = None,
    module: str | None = None,
) -> Any:
    """Return the most specific configured value for `key`.

    Lookup order, first hit wins:
        user → module → org → tenant → system → defined default
    """
    definition = get_definition(key)
    ck = _cache_key(key, tenant_id, org_node_id, user_id, module)
    with _CACHE_LOCK:
        if ck in _CACHE:
            return _CACHE[ck]

    candidates = []
    if user_id is not None:
        candidates.append((SettingScope.USER, str(user_id), tenant_id))
    if module is not None:
        candidates.append((SettingScope.MODULE, module, tenant_id))
    if org_node_id is not None:
        candidates.append((SettingScope.ORG, str(org_node_id), tenant_id))
    if tenant_id is not None:
        candidates.append((SettingScope.TENANT, "", tenant_id))
    candidates.append((SettingScope.SYSTEM, "", None))

    value: Any = definition.default
    for scope, scope_id, tid in candidates:
        row = SettingValue.objects.filter(
            key=key, scope=scope, scope_id=scope_id, tenant_id=tid
        ).first()
        if row is not None:
            value = row.value
            break

    with _CACHE_LOCK:
        _CACHE[ck] = value
    return value


def set_value(
    key: str,
    value: Any,
    *,
    scope: str,
    scope_id: str = "",
    tenant_id: int | None = None,
    updated_by_id: int | None = None,
) -> SettingValue:
    definition = get_definition(key)
    if scope not in definition.scopes:
        raise SettingsError(f"setting {key!r} is not configurable at scope {scope!r}")
    coerced = definition.coerce(value)
    obj, _ = SettingValue.objects.update_or_create(
        key=key,
        scope=scope,
        scope_id=scope_id,
        tenant_id=tenant_id,
        defaults={"value": coerced, "updated_by_id": updated_by_id},
    )
    invalidate(key)
    return obj
