"""Settings web UI for the Jarvis bridge.

A small in-app page (served by the Flask bridge) to change models, language,
TTS and the LLM instructions WITHOUT editing files or rebuilding. Writes to the
runtime config JSON (JARVIS_CONFIG_PATH) that ``load_settings()`` reads, then
restarts the bridge (and TTS worker) via supervisord so changes take effect.

Internal-network use only (no auth, per deployment decision).
"""

from __future__ import annotations

import json
import os
import subprocess
import urllib.request
from pathlib import Path
from typing import Any, Dict

# Fields the UI manages. Each maps to a key in the runtime config JSON, with a
# label and an input kind for the form.
FIELDS = [
    ("ollama_chat_model", "LLM 모델", "model"),
    ("whisper_model", "STT(Whisper) 모델", "select:tiny,base,small,medium,large,large-v3"),
    ("tts_engine", "TTS 엔진", "select:edge,piper"),
    ("output_language", "출력 언어 (비우면 사용자 언어)", "text"),
    ("llm_thinking_enabled", "LLM 사고(thinking) 모드", "bool"),
    ("agentic_max_turns", "에이전트 최대 반복", "number:1:12:1"),
    ("llm_instructions", "LLM 추가 지침 (시스템 프롬프트에 덧붙임)", "textarea"),
]
_KEYS = [k for k, _, _ in FIELDS]


def _config_path() -> Path:
    p = os.environ.get("JARVIS_CONFIG_PATH")
    return Path(p).expanduser() if p else (Path.home() / ".config" / "jarvis" / "config.json")


def _persist_path() -> Path:
    """Persistent overrides on the data volume — survive container recreate.
    entrypoint.sh merges this back onto the env-rendered config at startup."""
    return Path(os.environ.get("JARVIS_SETTINGS_PATH") or "/data/jarvis-settings.json")


def _read_config() -> Dict[str, Any]:
    try:
        return json.loads(_config_path().read_text("utf-8"))
    except Exception:
        return {}


def _current() -> Dict[str, Any]:
    cfg = _read_config()
    out: Dict[str, Any] = {}
    for k in _KEYS:
        if k == "output_language":
            out[k] = cfg.get("output_language", os.environ.get("OUTPUT_LANGUAGE", ""))
        else:
            out[k] = cfg.get(k, "")
    return out


def _ollama_models() -> list[str]:
    base = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434").rstrip("/")
    try:
        with urllib.request.urlopen(f"{base}/api/tags", timeout=4) as r:
            data = json.loads(r.read())
        return sorted(m.get("name", "") for m in data.get("models", []) if m.get("name"))
    except Exception:
        return []


def _coerce(updates: Dict[str, Any]) -> Dict[str, Any]:
    clean: Dict[str, Any] = {}
    for k, v in updates.items():
        if k not in _KEYS:
            continue
        if k == "agentic_max_turns":
            try:
                v = int(v)
            except (TypeError, ValueError):
                continue
        elif k == "llm_thinking_enabled":
            v = str(v).lower() in ("1", "true", "on", "yes")
        clean[k] = v
    return clean


def _write_merge(path: Path, clean: Dict[str, Any]) -> None:
    cur: Dict[str, Any] = {}
    try:
        cur = json.loads(path.read_text("utf-8"))
    except Exception:
        cur = {}
    cur.update(clean)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(cur, ensure_ascii=False, indent=2), "utf-8")


def _save(updates: Dict[str, Any]) -> None:
    clean = _coerce(updates)
    # 1) persistent overrides (survive `docker compose up` recreate)
    _write_merge(_persist_path(), clean)
    # 2) runtime config so a bridge/worker restart picks it up immediately
    _write_merge(_config_path(), clean)


def _apply() -> str:
    # Restart the bridge AFTER this response is sent. Detached (new session) so
    # the bridge being killed mid-restart doesn't drop the restart itself, and
    # the HTTP client still receives this response. (Edge TTS has no worker.)
    try:
        subprocess.Popen(
            ["sh", "-c", "sleep 1; supervisorctl restart bridge"],
            start_new_session=True,
        )
        return "1초 후 브리지가 재시작되어 반영됩니다."
    except Exception as e:  # pragma: no cover
        return str(e)


_PAGE = """<!doctype html><html lang=ko><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Jarvis 설정</title><style>
body{font-family:system-ui,Segoe UI,Apple SD Gothic Neo,sans-serif;max-width:680px;margin:24px auto;padding:0 16px;color:#222}
h1{font-size:20px}label{display:block;margin:14px 0 4px;font-weight:600}
input,select,textarea{width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;font-size:14px;box-sizing:border-box}
textarea{min-height:90px}.row{margin-bottom:6px}.btns{margin-top:18px;display:flex;gap:8px}
button{padding:10px 16px;border:0;border-radius:8px;font-size:14px;cursor:pointer}
.save{background:#2d6cdf;color:#fff}.apply{background:#16a34a;color:#fff}
#msg{margin-top:12px;color:#16a34a;white-space:pre-wrap}.hint{color:#888;font-weight:400;font-size:12px}
</style></head><body>
<h1>⚙️ Jarvis 설정</h1>
<p class=hint>저장 후 [적용]을 누르면 브리지/TTS가 재시작되며 반영됩니다. (내부망 전용)</p>
<form id=f></form>
<div class=btns><button class=save type=button onclick=save()>저장</button>
<button class=apply type=button onclick=apply()>저장 후 적용(재시작)</button></div>
<div id=msg></div>
<script>
const FIELDS=__FIELDS__, MODELS=__MODELS__, CUR=__CUR__;
const f=document.getElementById('f');
for(const [k,label,kind] of FIELDS){
  const id='fld_'+k; let el;
  if(k==='ollama_chat_model' && MODELS.length){
    el=`<select id="${id}">`+MODELS.map(m=>`<option ${m===CUR[k]?'selected':''}>${m}</option>`).join('')+`</select>`;
  } else if(kind.startsWith('select:')){
    el='<select id="'+id+'">'+kind.slice(7).split(',').map(o=>`<option ${o===CUR[k]?'selected':''}>${o}</option>`).join('')+'</select>';
  } else if(kind==='textarea'){
    el=`<textarea id="${id}">${CUR[k]??''}</textarea>`;
  } else if(kind==='bool'){
    el=`<select id="${id}"><option value=false ${!CUR[k]?'selected':''}>off</option><option value=true ${CUR[k]?'selected':''}>on</option></select>`;
  } else if(kind.startsWith('number:')){
    const [mn,mx,st]=kind.slice(7).split(':');
    el=`<input id="${id}" type=number min=${mn} max=${mx} step=${st} value="${CUR[k]??''}">`;
  } else { el=`<input id="${id}" type=text value="${CUR[k]??''}">`; }
  f.insertAdjacentHTML('beforeend',`<div class=row><label>${label}</label>${el}</div>`);
}
function collect(){const o={};for(const [k] of FIELDS){o[k]=document.getElementById('fld_'+k).value;}return o;}
async function post(url){const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(collect())});return r.json();}
async function save(){const j=await post('/api/settings');document.getElementById('msg').textContent=j.ok?'저장됨':'오류: '+(j.error||'');}
async function apply(){await post('/api/settings');const j=await fetch('/api/settings/apply',{method:'POST'}).then(r=>r.json());document.getElementById('msg').textContent='적용: '+(j.result||j.error||'');}
</script></body></html>"""


def register(app) -> None:
    """Attach the settings routes to the Flask ``app``."""
    from flask import request, jsonify, Response

    @app.get("/settings")
    def _settings_page():  # noqa: ANN202
        html = (
            _PAGE.replace("__FIELDS__", json.dumps(FIELDS, ensure_ascii=False))
            .replace("__MODELS__", json.dumps(_ollama_models()))
            .replace("__CUR__", json.dumps(_current(), ensure_ascii=False))
        )
        return Response(html, mimetype="text/html")

    @app.get("/api/settings")
    def _get_settings():  # noqa: ANN202
        return jsonify({"ok": True, "settings": _current(), "models": _ollama_models()})

    @app.post("/api/settings")
    def _post_settings():  # noqa: ANN202
        data = request.get_json(silent=True) or {}
        try:
            _save(data)
            return jsonify({"ok": True})
        except Exception as e:  # pragma: no cover
            return jsonify({"ok": False, "error": str(e)}), 500

    @app.post("/api/settings/apply")
    def _apply_settings():  # noqa: ANN202
        return jsonify({"ok": True, "result": _apply()})
