# -*- coding: utf-8 -*-
"""利用システム 画面（ブラウザUI / end-to-end）。

標準ライブラリのみ。動画ファイルをアップロードすると、上流（RF-DETR可読性・最良フレーム・
濃度確定）→ 学習時と同じLLM画像評価 → 後段スコアリング まで通して結果を表示する。

起動:
    python app.py            # http://127.0.0.1:8000 を開く
    python app.py --port 8001 --no-open

必要環境: GPU / ffmpeg(PATH) / BytePlus APIキー(secrets/byteplus.key か 環境変数 ARK_API_KEY)。
初回推論時にモデルを読み込むため、最初の1件だけ時間がかかる。
"""
from __future__ import annotations
import argparse
import json
import threading
import time
import traceback
import urllib.parse
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

HERE = Path(__file__).resolve().parent
UPLOAD_DIR = HERE / "_uploads"
UPLOAD_DIR.mkdir(exist_ok=True)

# 重いパイプラインは遅延初期化（サーバ起動を速く保つ）
_PIPE = None
_PIPE_LOCK = threading.Lock()


def get_pipeline():
    global _PIPE
    with _PIPE_LOCK:
        if _PIPE is None:
            import pipeline_full
            _PIPE = pipeline_full.FullPipeline()
        return _PIPE


SLOTS = ["開店前", "17時〜19時"]
SLOT_OPTS = "".join(f'<option value="{s}">{s}</option>' for s in SLOTS)

PAGE = """<!DOCTYPE html>
<html lang="ja"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ラーメンスープ評価AI</title>
<style>
  :root{ --bg:#0f1419; --card:#1a2230; --line:#2c3a4f; --fg:#e6edf3; --mut:#8b98a9;
         --accent:#ff7a18; --ok:#2ecc71; --warn:#f0a020; }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--fg);
       font-family:"Segoe UI","Hiragino Kaku Gothic ProN",Meiryo,sans-serif;line-height:1.6}
  .wrap{max-width:760px;margin:0 auto;padding:30px 20px 70px}
  h1{font-size:23px;margin:0 0 4px} .sub{color:var(--mut);margin:0 0 26px;font-size:13px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px}
  label{display:block;font-size:13px;color:var(--mut);margin:14px 0 6px}
  label:first-child{margin-top:0}
  input,select{width:100%;padding:11px 13px;background:#0d1320;color:var(--fg);
               border:1px solid var(--line);border-radius:9px;font-size:14px}
  input[type=file]{padding:9px}
  .btn{margin-top:24px;width:100%;padding:15px;border:0;border-radius:11px;cursor:pointer;
       background:linear-gradient(135deg,#ff7a18,#ff4d4d);color:#fff;font-size:16px;font-weight:700}
  .btn:disabled{opacity:.5;cursor:not-allowed}
  #status{margin-top:18px;color:var(--mut);font-size:13px;min-height:20px}
  .spin{display:inline-block;width:14px;height:14px;border:2px solid var(--mut);
        border-top-color:var(--accent);border-radius:50%;animation:r .8s linear infinite;vertical-align:-2px;margin-right:8px}
  @keyframes r{to{transform:rotate(360deg)}}
  #result{margin-top:24px}
  .scoreCard{display:flex;gap:16px}
  .scoreBox{flex:1;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px;text-align:center}
  .scoreBox .lbl{color:var(--mut);font-size:13px}
  .scoreBox .val{font-size:42px;font-weight:800;margin:6px 0}
  .grade{display:inline-block;min-width:42px;padding:4px 14px;border-radius:9px;font-weight:800;font-size:20px}
  .gA{background:#1e7e34}.gB{background:#2f6db0}.gC{background:#b5852a}.gD{background:#8a3030}
  .banner{padding:16px 18px;border-radius:12px;font-weight:700;margin-bottom:16px}
  .b-ok{background:rgba(46,204,113,.12);border:1px solid var(--ok);color:#a8f0c4}
  .b-warn{background:rgba(240,160,32,.12);border:1px solid var(--warn);color:#ffd98a}
  .b-err{background:rgba(220,80,80,.12);border:1px solid #c44;color:#f3b0b0}
  details{margin-top:16px;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:12px 16px}
  summary{cursor:pointer;color:var(--mut);font-size:13px}
  table{width:100%;border-collapse:collapse;margin-top:10px;font-size:13px}
  td{padding:5px 8px;border-bottom:1px solid var(--line)} td:first-child{color:var(--mut);width:42%}
  .muted{color:var(--mut);font-size:12px}
</style></head>
<body><div class="wrap">
  <h1>🍜 ラーメンスープ評価AI</h1>
  <p class="sub">動画をアップロードすると、上流（RF-DETR可読性・最良フレーム・濃度）→ LLM画像評価 → 採点 まで自動で実行します。</p>
  <div class="card">
    <label>濃度（任意 / Brix。空なら濃度推定AIが判定）</label>
    <input id="brix" type="number" step="0.1" placeholder="例: 12（空欄可）">
    <label>時間帯</label>
    <select id="slot">__SLOT__</select>
    <label>動画ファイル</label>
    <input id="file" type="file" accept="video/*,.mov,.mp4">
    <button class="btn" id="go" onclick="run()">推論する</button>
    <div id="status"></div>
  </div>
  <div id="result"></div>
</div>
<script>
async function run(){
  const f=document.getElementById('file').files[0];
  const st=document.getElementById('status'), btn=document.getElementById('go');
  document.getElementById('result').innerHTML='';
  if(!f){ st.textContent='動画ファイルを選択してください。'; return; }
  const q=new URLSearchParams({
    name:f.name, brix:document.getElementById('brix').value, slot:document.getElementById('slot').value
  });
  btn.disabled=true;
  st.innerHTML='<span class="spin"></span>解析中…（初回はモデル読込で1〜2分かかることがあります）';
  const t0=Date.now();
  try{
    const r=await fetch('/api/infer?'+q.toString(),{method:'POST',body:f});
    const res=await r.json();
    st.textContent='完了（'+((Date.now()-t0)/1000).toFixed(1)+'秒）';
    render(res);
  }catch(e){ st.textContent=''; document.getElementById('result').innerHTML=
    '<div class="banner b-err">エラー: '+e+'</div>'; }
  finally{ btn.disabled=false; }
}
function gc(g){return {A:'gA',B:'gB',C:'gC',D:'gD'}[g]||'gD';}
function box(l,s,g){return `<div class="scoreBox"><div class="lbl">${l}</div>
  <div class="val">${s}</div><span class="grade ${gc(g)}">${g}</span></div>`;}
function rows(o){return Object.entries(o).map(([k,v])=>
  `<tr><td>${k}</td><td>${v===null||v===undefined?'-':v}</td></tr>`).join('');}
function render(res){
  if(res.error){document.getElementById('result').innerHTML=
    '<div class="banner b-err">サーバエラー: '+res.error+'</div>';return;}
  const up=res._upstream||{}, le=res._llm_eval||{};
  let html='';
  if(res.status==='scored'){
    html+='<div class="banner b-ok">✅ 採点しました</div>'+
      `<div class="scoreCard">${box('総合スコア',res.total_score,res.total_grade)}
       ${box('スープスコア',res.soup_score,res.soup_grade)}</div>
       <p class="muted">グレード基準: A≥80 / B 60-79 / C 21-59 / D &lt;21</p>`;
  }else{
    html+='<div class="banner b-warn">⚠ 要確認：'+(res.reason||'スコアできません')+
      '</div><p class="muted">判定不能のため点数化しません。撮り直し／職人確認へ。</p>';
  }
  html+=`<details open><summary>LLM画像評価（動画→最良フレーム）</summary><table>${rows(le)}</table></details>`;
  html+=`<details><summary>上流処理（RF-DETR可読性・濃度・フレーム）</summary><table>${rows(up)}</table></details>`;
  document.getElementById('result').innerHTML=html;
}
</script>
</body></html>"""


class Handler(BaseHTTPRequestHandler):
    def _send(self, code, body, ctype="text/html; charset=utf-8"):
        data = body.encode("utf-8") if isinstance(body, str) else body
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def _json(self, code, obj):
        self._send(code, json.dumps(obj, ensure_ascii=False), "application/json; charset=utf-8")

    def do_GET(self):
        if self.path in ("/", "/index.html"):
            self._send(200, PAGE.replace("__SLOT__", SLOT_OPTS))
        else:
            self._send(404, "not found")

    def do_POST(self):
        if not self.path.startswith("/api/infer"):
            self._send(404, "not found"); return
        try:
            qs = urllib.parse.urlparse(self.path).query
            q = urllib.parse.parse_qs(qs)
            name = (q.get("name", ["upload.mov"])[0]) or "upload.mov"
            brix = (q.get("brix", [""])[0]).strip()
            slot = (q.get("slot", [""])[0]).strip()
            ext = Path(name).suffix or ".mov"

            n = int(self.headers.get("Content-Length", 0))
            if n <= 0:
                self._json(400, {"error": "空のアップロードです"}); return
            data = self.rfile.read(n)
            # 同時アップロードの衝突回避にスレッドID＋時刻でファイル名を作る
            stamp = f"{threading.get_ident()}_{int(time.time()*1000)}"
            tmp = UPLOAD_DIR / f"upload_{stamp}{ext}"
            tmp.write_bytes(data)

            try:
                pipe = get_pipeline()
                result = pipe.run(tmp, brix=brix or None, slot=slot or None)
            finally:
                try:
                    tmp.unlink()
                except OSError:
                    pass
            self._json(200, result)
        except Exception as e:
            traceback.print_exc()
            self._json(500, {"error": str(e)})

    def log_message(self, *a):
        pass


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8000)
    ap.add_argument("--no-open", action="store_true")
    args = ap.parse_args()
    url = f"http://127.0.0.1:{args.port}"
    print("ラーメンスープ評価AI 画面を起動:", url)
    print("初回推論はモデル読込で時間がかかります。停止は Ctrl+C。")
    if not args.no_open:
        try:
            webbrowser.open(url)
        except Exception:
            pass
    ThreadingHTTPServer(("127.0.0.1", args.port), Handler).serve_forever()


if __name__ == "__main__":
    main()
