"""Apply persisted setting overrides on top of the env-based Settings.

The runtime treats DB overrides as the source of truth: env values are
loaded once via pydantic, then any matching keys from the
``app_setting_overrides`` table replace those attributes in-process.
This way the admin can flip flags, adjust prices or rename labels
without restarting the container.
"""

from __future__ import annotations

import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional

from sqlalchemy.orm import sessionmaker

from bot.app.web.admin_settings_manifest import (
    SettingField,
    coerce_value,
    get_field_by_key,
    manifest_keys,
)
from config.settings import Settings
from db.dal import app_settings_dal

logger = logging.getLogger(__name__)

APPEARANCE_OVERRIDE_KEYS = {
    "WEBAPP_LOGO_USE_EMOJI",
    "WEBAPP_LOGO_URL",
    "WEBAPP_LOGO_EMOJI",
    "WEBAPP_LOGO_EMOJI_FONT",
    "WEBAPP_FAVICON_USE_CUSTOM",
    "WEBAPP_FAVICON_URL",
    "WEBAPP_LOGO_FAVICON_URL",
    "WEBAPP_PRIMARY_COLOR",
}
APP_ROOT = Path(__file__).resolve().parents[3]
APPEARANCE_OVERRIDES_BACKUP_PATH = APP_ROOT / "data" / "webapp-logo" / "appearance-settings.json"


def _resolve_attribute_name(settings: Settings, key: str) -> Optional[str]:
    """Resolve the actual attribute name on the Settings model.

    Some settings expose their env name via ``alias`` (e.g. MONTH_1_ENABLED is
    aliased to "1_MONTH_ENABLED"). Lookups by either alias or attribute name
    should both succeed, with the attribute name returned in either case.
    """

    if hasattr(settings, key):
        return key

    fields = type(settings).model_fields
    for attr_name, field_info in fields.items():
        alias = getattr(field_info, "alias", None)
        if alias and alias == key:
            return attr_name
    return None


def _apply_to_provider_bundle(key: str, value: Any) -> bool:
    """Route an override into the matching provider config/presentation model.

    Provider modules own their env-config via BaseSettings subclasses; here
    we look up which one owns ``key`` and write the value into the right
    attribute on the right model.
    """
    from bot.payment_providers import (
        find_manifest_owner,
        get_provider_bundle,
        get_spec_presentation,
    )

    owner = find_manifest_owner(key)
    if owner is None:
        return False
    spec, manifest_field = owner
    if manifest_field.target == "presentation":
        target = get_spec_presentation(spec.id)
        if target is None:
            bundle = get_provider_bundle(spec.service_key)
            target = bundle.presentation if bundle else None
    else:
        bundle = get_provider_bundle(spec.service_key)
        target = bundle.config if bundle else None
    if target is None:
        return False
    attr_name = manifest_field.attr or key
    try:
        setattr(target, attr_name, value)
        return True
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("Failed to apply provider override %s=%r: %s", key, value, exc)
        return False


def _apply_value(settings: Settings, key: str, value: Any) -> bool:
    # Provider-owned keys go to provider models, not the central Settings.
    if _apply_to_provider_bundle(key, value):
        return True
    attr_name = _resolve_attribute_name(settings, key)
    if not attr_name:
        return False
    try:
        setattr(settings, attr_name, value)
        return True
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("Failed to apply override %s=%r: %s", key, value, exc)
        return False


def apply_overrides(settings: Settings, overrides: Dict[str, Any]) -> int:
    applied = 0
    for key, raw_value in overrides.items():
        field = get_field_by_key(key)
        if not field:
            continue
        try:
            coerced = coerce_value(field, raw_value)
        except ValueError as exc:
            logger.warning("Skipping override %s: %s", key, exc)
            continue
        if _apply_value(settings, key, coerced):
            applied += 1
    return applied


def _normalize_exclusive_provider_toggles(
    updates: Dict[str, Any],
    deletes: list,
) -> tuple[Dict[str, Any], list]:
    """When a provider is enabled for admins only, turn off its public toggle."""

    from bot.payment_providers import provider_admin_only_pairs

    exclusive_map = {
        key: opposite
        for public_key, admin_key in provider_admin_only_pairs()
        for key, opposite in ((public_key, admin_key), (admin_key, public_key))
    }
    if not exclusive_map:
        return updates, deletes

    normalized = dict(updates)
    normalized_deletes = list(deletes)
    for key, value in updates.items():
        if value is not True or key not in exclusive_map:
            continue
        opposite = exclusive_map[key]
        normalized[opposite] = False
        normalized_deletes = [item for item in normalized_deletes if item != opposite]
    return normalized, normalized_deletes


def _appearance_snapshot(settings: Settings) -> Dict[str, Any]:
    snapshot: Dict[str, Any] = {}
    logo_url = getattr(settings, "WEBAPP_LOGO_URL", None)
    logo_favicon_url = getattr(settings, "WEBAPP_LOGO_FAVICON_URL", None)
    favicon_url = getattr(settings, "WEBAPP_FAVICON_URL", None)
    if logo_url:
        snapshot["WEBAPP_LOGO_URL"] = logo_url
    if logo_favicon_url:
        snapshot["WEBAPP_LOGO_FAVICON_URL"] = logo_favicon_url
    if favicon_url:
        snapshot["WEBAPP_FAVICON_URL"] = favicon_url
    if getattr(settings, "WEBAPP_FAVICON_USE_CUSTOM", False):
        snapshot["WEBAPP_FAVICON_USE_CUSTOM"] = True
    if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
        snapshot["WEBAPP_LOGO_USE_EMOJI"] = True
        snapshot["WEBAPP_LOGO_EMOJI"] = getattr(settings, "WEBAPP_LOGO_EMOJI", "")
        emoji_font = getattr(settings, "WEBAPP_LOGO_EMOJI_FONT", "")
        if emoji_font and emoji_font != "system":
            snapshot["WEBAPP_LOGO_EMOJI_FONT"] = emoji_font
    primary_color = getattr(settings, "WEBAPP_PRIMARY_COLOR", None)
    if primary_color and primary_color != "#00fe7a":
        snapshot["WEBAPP_PRIMARY_COLOR"] = primary_color
    return snapshot


def _read_appearance_backup() -> Dict[str, Any]:
    try:
        payload = json.loads(APPEARANCE_OVERRIDES_BACKUP_PATH.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    except (OSError, json.JSONDecodeError) as exc:
        logger.warning("Failed to read appearance settings backup: %s", exc)
        return {}
    if not isinstance(payload, dict):
        return {}
    values = payload.get("settings") if isinstance(payload.get("settings"), dict) else payload
    restored: Dict[str, Any] = {}
    for key, value in values.items():
        if key not in APPEARANCE_OVERRIDE_KEYS:
            continue
        if value in (None, "") or value is False:
            continue
        field = get_field_by_key(key)
        if not field:
            continue
        try:
            restored[key] = coerce_value(field, value)
        except ValueError as exc:
            logger.warning("Skipping appearance backup key %s: %s", key, exc)
    return restored


def write_appearance_backup(settings: Settings) -> None:
    payload = {
        "version": 1,
        "settings": _appearance_snapshot(settings),
    }
    try:
        APPEARANCE_OVERRIDES_BACKUP_PATH.parent.mkdir(parents=True, exist_ok=True)
        APPEARANCE_OVERRIDES_BACKUP_PATH.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
    except OSError as exc:
        logger.warning("Failed to write appearance settings backup: %s", exc)


async def load_overrides_from_db(settings: Settings, async_session_factory: sessionmaker) -> int:
    """Fetch overrides from the DB and apply them to the in-memory settings.

    Provider env-configs live on per-provider BaseSettings bundles instead of
    the central Settings model. Apply needs those bundles to already exist,
    otherwise provider-owned overrides (e.g. ``HELEKET_ENABLED``) silently
    drop on the floor. Build them up-front; the call is idempotent so the
    later ``build_core_services`` invocation reuses these same instances.
    """
    from bot.payment_providers import build_provider_configs

    build_provider_configs()

    try:
        async with async_session_factory() as session:
            overrides = await app_settings_dal.get_all_overrides(session)
            backup_overrides = _read_appearance_backup()
            missing_backup_overrides = {
                key: value for key, value in backup_overrides.items() if key not in overrides
            }
            if missing_backup_overrides:
                for key, value in missing_backup_overrides.items():
                    await app_settings_dal.upsert_override(
                        session, key=key, value=value, updated_by=None
                    )
                await session.commit()
                overrides.update(missing_backup_overrides)
                logger.info(
                    "Restored %s appearance setting overrides from %s",
                    len(missing_backup_overrides),
                    APPEARANCE_OVERRIDES_BACKUP_PATH,
                )
    except Exception as exc:
        logger.warning("Could not load setting overrides from DB: %s", exc)
        return 0

    applied = apply_overrides(settings, overrides)
    if applied:
        logger.info("Applied %s setting overrides from DB", applied)
    return applied


async def refresh_overrides_from_db(
    settings: Settings,
    async_session_factory: sessionmaker,
    *,
    keys: Optional[set[str]] = None,
) -> int:
    """Refresh already-known runtime overrides without startup restore side effects."""

    try:
        async with async_session_factory() as session:
            overrides = await app_settings_dal.get_all_overrides(session)
    except Exception as exc:
        logger.warning("Could not refresh setting overrides from DB: %s", exc)
        return 0
    if keys is not None:
        try:
            env_only = Settings()
            for key in keys:
                if key in overrides:
                    continue
                attr_name = _resolve_attribute_name(env_only, key)
                if attr_name and hasattr(env_only, attr_name):
                    setattr(settings, attr_name, getattr(env_only, attr_name))
        except Exception as exc:
            logger.warning("Failed to restore env defaults while refreshing overrides: %s", exc)
        overrides = {key: value for key, value in overrides.items() if key in keys}
    return apply_overrides(settings, overrides)


async def update_overrides(
    settings: Settings,
    async_session_factory: sessionmaker,
    *,
    updates: Dict[str, Any],
    deletes: Optional[list] = None,
    actor_id: Optional[int] = None,
) -> Dict[str, Any]:
    """Persist + apply a batch of changes coming from the admin UI."""

    deletes = list(deletes or [])
    coerced_updates: Dict[str, Any] = {}
    errors: Dict[str, str] = {}

    for key, raw in updates.items():
        field: Optional[SettingField] = get_field_by_key(key)
        if not field:
            errors[key] = "unknown_setting"
            continue
        try:
            coerced_updates[key] = coerce_value(field, raw)
        except ValueError as exc:
            errors[key] = str(exc)

    valid_deletes = []
    for key in deletes:
        if get_field_by_key(key) is None:
            errors.setdefault(key, "unknown_setting")
            continue
        valid_deletes.append(key)

    if errors:
        return {"ok": False, "errors": errors}

    coerced_updates, valid_deletes = _normalize_exclusive_provider_toggles(
        coerced_updates,
        valid_deletes,
    )

    async with async_session_factory() as session:  # type: AsyncSession
        async with session.begin():
            for key, value in coerced_updates.items():
                await app_settings_dal.upsert_override(
                    session, key=key, value=value, updated_by=actor_id
                )
            for key in valid_deletes:
                await app_settings_dal.delete_override(session, key)

    # Apply locally; deletes need an env-default fallback. We re-read the env
    # default by instantiating a fresh Settings() / provider-config model
    # (cheap; just a few ms) and copying the matching attributes back over.
    if valid_deletes:
        from bot.payment_providers import (
            find_manifest_owner,
            get_provider_bundle,
            get_spec_presentation,
        )

        try:
            env_only = Settings()
            for key in valid_deletes:
                owner = find_manifest_owner(key)
                if owner is not None:
                    spec, manifest_field = owner
                    if manifest_field.target == "presentation":
                        target = get_spec_presentation(spec.id)
                        if target is None:
                            bundle = get_provider_bundle(spec.service_key)
                            target = bundle.presentation if bundle else None
                    else:
                        bundle = get_provider_bundle(spec.service_key)
                        target = bundle.config if bundle else None
                    if target is None:
                        continue
                    cls = type(target)
                    try:
                        fresh = cls()
                    except Exception as exc:
                        logger.warning(
                            "Failed to reload provider env defaults for %s: %s",
                            key,
                            exc,
                        )
                        continue
                    attr = manifest_field.attr or key
                    if hasattr(fresh, attr):
                        setattr(target, attr, getattr(fresh, attr))
                    continue
                attr_name = _resolve_attribute_name(env_only, key) or key
                if hasattr(env_only, attr_name):
                    setattr(settings, attr_name, getattr(env_only, attr_name))
        except Exception as exc:  # pragma: no cover - defensive
            logger.warning("Failed to restore env defaults: %s", exc)

    apply_overrides(settings, coerced_updates)
    appearance_changed = APPEARANCE_OVERRIDE_KEYS.intersection(
        coerced_updates
    ) or APPEARANCE_OVERRIDE_KEYS.intersection(valid_deletes)
    if appearance_changed:
        write_appearance_backup(settings)

    return {"ok": True, "applied": len(coerced_updates), "reverted": len(valid_deletes)}


def overridable_keys() -> list:
    return list(manifest_keys())


def current_value(settings: Settings, key: str) -> Any:
    # Provider-owned keys live on per-provider BaseSettings bundles, not the
    # central Settings — check there first.
    from bot.payment_providers import (
        find_manifest_owner,
        get_provider_bundle,
        get_spec_presentation,
    )

    owner = find_manifest_owner(key)
    if owner is not None:
        spec, manifest_field = owner
        if manifest_field.target == "presentation":
            target = get_spec_presentation(spec.id)
            if target is None:
                bundle = get_provider_bundle(spec.service_key)
                target = bundle.presentation if bundle else None
        else:
            bundle = get_provider_bundle(spec.service_key)
            target = bundle.config if bundle else None
        if target is not None:
            attr = manifest_field.attr or key
            return getattr(target, attr, None)
        return None

    attr_name = _resolve_attribute_name(settings, key)
    if not attr_name:
        return None
    return getattr(settings, attr_name, None)
