"""Admin endpoints for AI provider selection and API-key management.

* ``GET  /api/ai/settings/providers`` — current provider per capability,
  redacted view of which API keys are present (booleans only — keys are
  *never* echoed back), and the list of allowed providers per capability.

* ``PATCH /api/ai/settings/providers`` — write provider choices and/or
  API keys to ``ir.config_parameter``. Settings take effect on the very
  next request (no caching), so admins can flip providers without an
  Odoo restart.

The controller is admin-gated:

* The caller must be authenticated (``@jwt_required``).
* The caller must have ``user_type == 'admin'`` *or* be in the
  ``base.group_system`` group. Anything else returns 403.

API-key fields are write-only over the wire. Sending an empty string
clears the key; omitting the field leaves it unchanged. The GET response
returns ``{"openai_key_set": true | false, ...}`` markers so the UI can
render a "saved · click to replace" state without ever leaking the
secret value.
"""

from __future__ import annotations

import json
import logging

from odoo import http
from odoo.http import request, Response

from odoo.addons.encoach_api.controllers.base import (
    jwt_required, _json_response, _error_response, _get_json_body,
)
from odoo.addons.encoach_ai.services import provider_router

_logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Configuration tables
# ---------------------------------------------------------------------------

# Provider keys per capability — mirrors provider_router.CAPABILITIES but
# adds UI labels and the "kind" so the frontend can render appropriate
# icons / disclaimers.
_PROVIDER_OPTIONS = {
    'text': [
        {'value': 'openai', 'label': 'OpenAI (GPT-4o)', 'kind': 'paid'},
        {'value': 'mock',   'label': 'Mock (deterministic stub)', 'kind': 'free'},
    ],
    'image': [
        {'value': 'auto',     'label': 'Auto (paid → free fallback)', 'kind': 'auto'},
        {'value': 'openai',   'label': 'OpenAI (DALL-E 3)', 'kind': 'paid'},
        {'value': 'pillow',   'label': 'Pillow placeholder (offline)', 'kind': 'free'},
        {'value': 'unsplash', 'label': 'Unsplash Source (free, network)', 'kind': 'free'},
        {'value': 'mock',     'label': 'Mock card', 'kind': 'free'},
    ],
    'audio': [
        {'value': 'auto',       'label': 'Auto (paid → free fallback)', 'kind': 'auto'},
        {'value': 'polly',      'label': 'AWS Polly (neural)', 'kind': 'paid'},
        {'value': 'elevenlabs', 'label': 'ElevenLabs (multilingual)', 'kind': 'paid'},
        {'value': 'gtts',       'label': 'gTTS (free, network)', 'kind': 'free'},
        {'value': 'silent',     'label': 'Silent stub (offline)', 'kind': 'free'},
    ],
    'video': [
        {'value': 'auto',   'label': 'Auto', 'kind': 'auto'},
        {'value': 'ffmpeg', 'label': 'ffmpeg slideshow (image+audio)', 'kind': 'free'},
        {'value': 'static', 'label': 'Static placeholder image', 'kind': 'free'},
    ],
}

# API-key params managed by this endpoint. Keys are write-only — the
# response only ever returns ``<name>_set: bool``.
_KEY_PARAMS = {
    'openai_api_key':       'encoach_ai.openai_api_key',
    'aws_access_key':       'encoach_ai.aws_access_key',
    'aws_secret_key':       'encoach_ai.aws_secret_key',
    'aws_region':           'encoach_ai.aws_region',
    'elevenlabs_api_key':   'encoach_ai.elevenlabs_api_key',
    'gptzero_api_key':      'encoach_ai.gptzero_api_key',
    # Paymob (payments) — included so all platform secrets live in one UI
    'paymob_api_key':       'encoach.paymob.api_key',
    'paymob_integration_id': 'encoach.paymob.integration_id',
    'paymob_iframe_id':     'encoach.paymob.iframe_id',
    'paymob_hmac_secret':   'encoach.paymob.hmac_secret',
}

# These params don't carry secrets so we expose their plaintext values.
_PLAIN_PARAMS = {
    'aws_region':       'encoach_ai.aws_region',
    'openai_model':     'encoach_ai.openai_model',
    'openai_fast_model': 'encoach_ai.openai_fast_model',
    'elevenlabs_model': 'encoach_ai.elevenlabs_model',
    'request_timeout':  'encoach_ai.request_timeout',
    'max_retries':      'encoach_ai.max_retries',
    'enabled':          'encoach_ai.enabled',
}


# ---------------------------------------------------------------------------
# Authorization
# ---------------------------------------------------------------------------


def _is_admin(env):
    """Return True if the calling user is an EnCoach admin or system admin."""
    user = env.user
    if not user or not user.id:
        return False
    if user.has_group('base.group_system'):
        return True
    user_type = getattr(user, 'user_type', None)
    return user_type == 'admin'


# ---------------------------------------------------------------------------
# Serialization helpers
# ---------------------------------------------------------------------------


def _read_state(env):
    """Build the full settings payload (no secrets in the output)."""
    Param = env['ir.config_parameter'].sudo()
    providers = {}
    for cap, options in _PROVIDER_OPTIONS.items():
        providers[cap] = {
            'active': provider_router.get_active_provider(env, cap),
            'options': options,
            'paid_with_credentials': provider_router.get_paid_provider_keys(
                env, cap,
            ),
        }
    keys_set = {}
    for short, full_param in _KEY_PARAMS.items():
        # ``aws_region`` happens to live in both maps — it's not a secret,
        # so we surface its value in ``plain`` and *also* mark it as set so
        # the UI can show the field consistently.
        val = Param.get_param(full_param)
        keys_set[short] = bool(val)
    plain = {short: Param.get_param(p, '')
             for short, p in _PLAIN_PARAMS.items()}
    return {
        'providers': providers,
        'keys_set': keys_set,
        'plain': plain,
    }


def _is_clear_marker(value):
    """A trimmed, empty string explicitly clears the param."""
    return isinstance(value, str) and value.strip() == ''


# ---------------------------------------------------------------------------
# Controller
# ---------------------------------------------------------------------------


class AISettingsController(http.Controller):
    """REST surface for the AI Provider Settings admin page."""

    @http.route('/api/ai/settings/providers',
                type='http', auth='none', methods=['GET', 'OPTIONS'],
                csrf=False)
    @jwt_required
    def get_providers(self, **kw):
        if not _is_admin(request.env):
            return _error_response('Admin access required', 403)
        try:
            return _json_response({'data': _read_state(request.env)})
        except Exception as exc:
            _logger.exception('ai_settings.get failed')
            return _error_response(str(exc), 500)

    @http.route('/api/ai/settings/providers',
                type='http', auth='none', methods=['PATCH', 'POST', 'PUT'],
                csrf=False)
    @jwt_required
    def patch_providers(self, **kw):
        if not _is_admin(request.env):
            return _error_response('Admin access required', 403)
        try:
            body = _get_json_body() or {}
            Param = request.env['ir.config_parameter'].sudo()

            # 1. Update active provider per capability — validate that the
            #    chosen value is one of the offered options to keep junk
            #    out of ir.config_parameter.
            provider_updates = body.get('providers') or {}
            invalid = []
            for cap, value in provider_updates.items():
                if cap not in _PROVIDER_OPTIONS:
                    invalid.append(f'unknown capability: {cap}')
                    continue
                allowed = {o['value'] for o in _PROVIDER_OPTIONS[cap]}
                if value not in allowed:
                    invalid.append(f'{cap}: {value!r} not in {sorted(allowed)}')
                    continue
                Param.set_param(provider_router.CAPABILITIES[cap]['param'], value)
            if invalid:
                return _error_response(
                    'Invalid provider selections: ' + '; '.join(invalid), 400,
                )

            # 2. Update API keys — write-only. An empty string clears the
            #    param; omitting the field leaves it untouched.
            key_updates = body.get('keys') or {}
            for short, value in key_updates.items():
                if short not in _KEY_PARAMS:
                    continue
                full_param = _KEY_PARAMS[short]
                if value is None:
                    continue
                if _is_clear_marker(value):
                    Param.set_param(full_param, '')
                else:
                    # Trim whitespace to defend against a copy-paste with
                    # a trailing newline that would silently break SDK auth.
                    Param.set_param(full_param, str(value).strip())

            # 3. Plain-value updates (model names, region, timeout, ...).
            plain_updates = body.get('plain') or {}
            for short, value in plain_updates.items():
                if short not in _PLAIN_PARAMS:
                    continue
                Param.set_param(_PLAIN_PARAMS[short], '' if value is None
                                else str(value))

            # Audit log so admins can see who flipped providers.
            try:
                _logger.info(
                    'ai_settings.update by user_id=%s providers=%s '
                    'keys_changed=%s plain_changed=%s',
                    request.env.user.id,
                    list(provider_updates.keys()),
                    [k for k in key_updates.keys() if k in _KEY_PARAMS],
                    list(plain_updates.keys()),
                )
            except Exception:
                pass

            return _json_response({'data': _read_state(request.env)})
        except Exception as exc:
            _logger.exception('ai_settings.patch failed')
            return _error_response(str(exc), 500)

    @http.route('/api/ai/settings/providers/test',
                type='http', auth='none', methods=['POST'], csrf=False)
    @jwt_required
    def test_provider(self, **kw):
        """Quick "is this configured?" probe for the UI's Test button.

        Body: ``{"capability": "image" | "audio" | "text"}``.

        Returns the resolved provider chain plus a flag for each entry
        indicating whether credentials are present. We deliberately do
        NOT make a real network call — we just resolve the chain and
        check for credentials so the test is instant and free.
        """
        if not _is_admin(request.env):
            return _error_response('Admin access required', 403)
        try:
            body = _get_json_body() or {}
            capability = body.get('capability') or 'image'
            if capability not in provider_router.CAPABILITIES:
                return _error_response(
                    f'Unknown capability: {capability}', 400,
                )
            chain = provider_router.resolve_chain(request.env, capability)
            paid_with_creds = set(
                provider_router.get_paid_provider_keys(request.env, capability),
            )
            entries = []
            for prov in chain:
                if prov in provider_router.CAPABILITIES[capability]['paid']:
                    ok = prov in paid_with_creds
                    note = 'credentials configured' if ok else 'no API key'
                else:
                    ok = True  # free providers are always available
                    note = 'free fallback'
                entries.append({'provider': prov, 'ok': ok, 'note': note})
            return _json_response({
                'capability': capability,
                'active': provider_router.get_active_provider(
                    request.env, capability),
                'chain': entries,
            })
        except Exception as exc:
            _logger.exception('ai_settings.test failed')
            return _error_response(str(exc), 500)
