"""AppConfig service — typed access to structured application settings.

Replaces the key-value system_settings table with JSONB-column app_config.
Both old and new APIs are supported during migration; system_settings is kept
as a read-only backward-compat layer.

Usage:
    from app.core.config_service import get_app_config, update_render_config

    # Async (FastAPI handlers)
    config = await get_app_config(db)
    config.render.thumbnail_renderer  # → "blender"

    # Sync (Celery tasks)
    from app.core.config_service import get_app_config_sync
    config = get_app_config_sync()
"""
import json
import logging
from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Typed config models
# ---------------------------------------------------------------------------

class RenderConfig(BaseModel):
    thumbnail_renderer: str = "blender"
    blender_engine: str = "cycles"
    blender_cycles_samples: int = 256
    blender_eevee_samples: int = 64
    thumbnail_format: str = "jpg"
    stl_quality: str = "low"
    blender_smooth_angle: int = 30
    cycles_device: str = "auto"
    render_backend: str = "celery"
    product_thumbnail_priority: list[str] = Field(
        default_factory=lambda: ["latest_render", "cad_thumbnail"]
    )


class WorkerConfig(BaseModel):
    concurrency: int = 8
    max_concurrent_renders: int = 3
    render_stall_timeout_minutes: int = 120


class StorageConfig(BaseModel):
    upload_dir: str = "/app/uploads"
    max_upload_size_mb: int = 500


class NotificationsConfig(BaseModel):
    pass  # reserved for Phase G


class BillingConfig(BaseModel):
    pass  # reserved for Phase L


class AppConfig(BaseModel):
    render: RenderConfig = Field(default_factory=RenderConfig)
    storage: StorageConfig = Field(default_factory=StorageConfig)
    notifications: NotificationsConfig = Field(default_factory=NotificationsConfig)
    worker: WorkerConfig = Field(default_factory=WorkerConfig)
    billing: BillingConfig = Field(default_factory=BillingConfig)
    updated_at: datetime | None = None


# ---------------------------------------------------------------------------
# DB access
# ---------------------------------------------------------------------------

_SELECT_SQL = "SELECT render, storage, notifications, worker, billing, updated_at FROM app_config ORDER BY version DESC LIMIT 1"
_DEFAULT_CONFIG = AppConfig()


async def get_app_config(db: AsyncSession) -> AppConfig:
    """Load AppConfig from app_config table (async)."""
    try:
        result = await db.execute(text(_SELECT_SQL))
        row = result.one_or_none()
        return _row_to_config(row)
    except Exception as exc:
        logger.warning("get_app_config failed, returning defaults: %s", exc)
        return _DEFAULT_CONFIG


def get_app_config_sync(session: Session | None = None) -> AppConfig:
    """Load AppConfig from app_config table (sync, for Celery tasks)."""
    try:
        if session:
            row = session.execute(text(_SELECT_SQL)).one_or_none()
            return _row_to_config(row)

        from app.config import settings as app_settings
        from sqlalchemy import create_engine
        engine = create_engine(app_settings.database_url_sync)
        with Session(engine) as s:
            row = s.execute(text(_SELECT_SQL)).one_or_none()
        engine.dispose()
        return _row_to_config(row)
    except Exception as exc:
        logger.warning("get_app_config_sync failed, returning defaults: %s", exc)
        return _DEFAULT_CONFIG


async def update_render_config(db: AsyncSession, updates: dict[str, Any]) -> AppConfig:
    """Merge updates into the render section of app_config."""
    return await _update_section(db, "render", updates)


async def update_worker_config(db: AsyncSession, updates: dict[str, Any]) -> AppConfig:
    """Merge updates into the worker section of app_config."""
    return await _update_section(db, "worker", updates)


async def _update_section(db: AsyncSession, section: str, updates: dict[str, Any]) -> AppConfig:
    valid_sections = {"render", "storage", "notifications", "worker", "billing"}
    if section not in valid_sections:
        raise ValueError(f"Invalid config section: {section}")

    await db.execute(
        text(f"""
            UPDATE app_config
            SET {section} = {section} || :patch::jsonb,
                updated_at = NOW()
        """),
        {"patch": json.dumps(updates)},
    )
    await db.commit()
    return await get_app_config(db)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _row_to_config(row) -> AppConfig:
    if row is None:
        return _DEFAULT_CONFIG

    def _parse(raw, model_cls):
        if raw is None:
            return model_cls()
        if isinstance(raw, str):
            raw = json.loads(raw)
        try:
            return model_cls(**raw)
        except Exception:
            return model_cls()

    return AppConfig(
        render=_parse(row[0], RenderConfig),
        storage=_parse(row[1], StorageConfig),
        notifications=_parse(row[2], NotificationsConfig),
        worker=_parse(row[3], WorkerConfig),
        billing=_parse(row[4], BillingConfig),
        updated_at=row[5],
    )
