# -*- coding: utf-8 -*-
"""
ラーメン豚山 スープ評価スコアリング・プロトタイプ (A)
============================================================
職人の採点ロジックを人間採点データ(686件, analysis.py で再計算)から近似したスコア関数。

構造（職人の2段階採点を踏襲）:
    最終スコア = 状態スコア(画像特徴) + 濃度補正(Brix, 時間帯)
    濃度補正は【非対称】: 目標未満のみ強く減点 / 目標以上は罰しない（下記係数参照）

重みの根拠（analysis.py の回帰, コメント由来の擬似特徴量ベース）:
    回帰 R^2=0.560 / MAE=6.98。プロトタイプ単体の人間スコア再現は
    R^2=0.399 / MAE=9.8（validate_prototype.py）。
注意:
  - 状態スコアの重みは「画像特徴」ではなく「評価備考に現れた指摘語」とスコアの関係から
    得た擬似値。画像特徴量としては未検証（feature_design.md 参照）。実運用前に
    実画像での抽出と職人点との相関確認が必要。
  - データには total>soup の加点ケースが73件あるが、本式の濃度補正は超過時に
    加点しない（BRIX_PENALTY_OVER=0 の安全形）。加点は状態スコア側または別の
    総合判断で表現されるもので、本式が加点ケースそのものを再現するわけではない。
  - 将来、画像特徴量が入力できるようになれば VisualFeatures を実測値に差し替えて運用可。
  - 入力は静止画像を前提（動画専用特徴=対流/フレーム間差分/オプティカルフローは使わない）。
"""
from dataclasses import dataclass, field

# 時間帯ごとの目標Brix（満点ライン。データの満点群最頻値より。analysis.py §5）
TARGET_BRIX = {"開店前": 13.0, "ディナー": 12.0}

# 濃度補正は【非対称】（analysis.py §6,§7 で確認）:
#   - 目標"未満"のときだけ強く減点（回帰 gap_under係数 ≈ -7.6 / Brix1あたり）
#   - 目標"以上"はほぼ罰しない（回帰 gap_over係数 ≈ +0.2 ≒ 0）
# ※ 旧版の「対称な山型(高すぎも減点)」は誤り。データは片側ペナルティを示す。
BRIX_PENALTY_UNDER = 7.6   # 目標未満1.0あたりの減点
BRIX_PENALTY_OVER  = 0.0   # 目標超過は減点しない（わずかに加点傾向だが0で安全側）

# 状態スコアの加減点テーブル（回帰係数より。基準=80点スタート）
STATE_BASE = 80.0
STATE_WEIGHTS = {
    # プラス要因
    "good_emulsion":   +16.0,  # 微乳化が決まっている（満点ゾーン）
    # マイナス要因
    "oil_excess":       -7.5,  # アブラ感が強い・重い（脂過多）
    "separation":       -6.0,  # 分離・油層・白すぎ・シャバい（乳化不足/過多）
    "meat_collapse":    -5.0,  # ボロ・シロ崩れ（豚の崩れ）
    "gara_weak":        -4.0,  # ガラ感不足
    "light":            -3.3,  # 軽い・アブラ不足
    "low_water":        -3.0,  # 水位が低い
    "thin_look":        -2.4,  # 見た目が薄い
    "fire_strong":      -1.0,  # 火が強い（軽微。静止画では弱い代理＝湯気量/大気泡）
}

# 撮影ノイズ：味ではなく運用上の問題。味スコアと分離して扱う（回帰: unseen ≈ -10）
UNUSABLE_PENALTY = -10.0


@dataclass
class VisualFeatures:
    """静止画像から推定する状態特徴（0.0〜1.0の確信度）。
    将来 画像CVモデルの出力をそのまま入れる。手動評価でも 0/1 で使える。
    ※Brixはここに含めない（画像推定せず濃度計の実測値を score_soup に別入力）。"""
    good_emulsion: float = 0.0   # 微乳化が理想的に決まっている度
    oil_excess: float = 0.0      # 脂過多度
    separation: float = 0.0      # 分離・シャバ・白すぎ度
    meat_collapse: float = 0.0   # 豚(シロ)の崩れ度
    gara_weak: float = 0.0       # ガラ感不足度
    light: float = 0.0           # 軽さ（脂不足）度
    low_water: float = 0.0       # 水位の低さ
    thin_look: float = 0.0       # 見た目の薄さ
    fire_strong: float = 0.0     # 火力過多度
    unusable: float = 0.0        # 撮影不能度(静止画判定: 白飛び/暗すぎ/被写体不足/極端な角度)


@dataclass
class ScoreResult:
    state_score: float
    brix_penalty: float
    total_score: float
    usable: bool
    breakdown: dict = field(default_factory=dict)


def slot_to_target(slot: str) -> float:
    """時間帯文字列から目標Brixを返す。'17時～19時'等はディナー扱い。"""
    return TARGET_BRIX["開店前"] if "開店前" in str(slot) else TARGET_BRIX["ディナー"]


def score_soup(features: VisualFeatures, brix: float, slot: str,
               evaluator_bias: float = 0.0) -> ScoreResult:
    """
    features      : 画像から推定した状態特徴
    brix          : 濃度計の測定値(Brix)
    slot          : 時間帯('開店前' or 'ディナー'系)
    evaluator_bias: 評価者正規化用オフセット(下の EVALUATOR_BIAS 参照)。
                    AI運用では 0（バイアス除去済みの基準で採点）。
    """
    bd = {}

    # --- 撮影ノイズ判定（味評価の前段）---
    usable = features.unusable < 0.5
    noise = UNUSABLE_PENALTY * features.unusable
    bd["撮影ノイズ"] = round(noise, 1)

    # --- ① 状態スコア（見た目）---
    state = STATE_BASE
    for key, w in STATE_WEIGHTS.items():
        contrib = w * getattr(features, key)
        if contrib:
            bd[key] = round(contrib, 1)
        state += contrib
    state += noise + evaluator_bias
    state = max(0.0, min(100.0, state))

    # --- ② 濃度補正（非対称：未満のみ減点）---
    target = slot_to_target(slot)
    under = max(0.0, target - brix)   # 目標に足りない量
    over  = max(0.0, brix - target)   # 目標を超えた量
    penalty = -BRIX_PENALTY_UNDER * under - BRIX_PENALTY_OVER * over
    bd["濃度ペナルティ"] = round(penalty, 1)
    bd[f"目標Brix({'開店前' if target==13 else 'ディナー'})"] = target

    total = max(0.0, min(100.0, state + penalty))

    return ScoreResult(round(state, 1), round(penalty, 1), round(total, 1),
                       usable, bd)


# ===== 評価者別の素点平均（参考値・交絡あり）=====
# 注意: これは「評価者の甘辛」と「担当店のスープ実力」が交絡した値であり、
#       純粋なバイアスではない（1評価者=1店舗で固定のため分離不能）。
#       藤田(神保町)を基準にした soup平均の差。analysis.py §3 で再現可能。
#       AI運用では 0 を使う（バイアス補正は多評価者×同一店のデータ収集後に実施）。
EVALUATOR_SOUP_DELTA = {  # 参考のみ。score_soup には既定で渡さない。
    "藤田　潤":     0.0,    # soup平均 93.4 (n=315) 基準
    "垣内　孝太": -10.7,    # soup平均 82.7 (n=37)
    "川村　純史": -14.4,    # soup平均 79.0 (n=212)
    "稲葉　繁":   -23.2,    # soup平均 70.2 (n=67)
    "西島　功之助":-32.7,   # soup平均 60.7 (n=55)
}
EVALUATOR_BIAS = EVALUATOR_SOUP_DELTA  # 後方互換エイリアス


if __name__ == "__main__":
    # デモ：理想的な微乳化・Brix13・開店前
    ideal = VisualFeatures(good_emulsion=1.0)
    print("理想(微乳化/Brix13/開店前):", score_soup(ideal, 13, "開店前").total_score)

    # 脂過多・水位低め・Brix11・ディナー（目標12より低い→減点）
    oily = VisualFeatures(oil_excess=1.0, low_water=0.6)
    r = score_soup(oily, 11, "17時～19時")
    print("脂過多/水位低/Brix11/ディナー:", r.total_score, r.breakdown)

    # 微乳化・Brix14・開店前（目標13超過→減点しない＝非対称の確認）
    hi = VisualFeatures(good_emulsion=1.0)
    print("微乳化/Brix14/開店前(超過は罰しない):", score_soup(hi, 14, "開店前").total_score)

    # 撮影不備
    bad = VisualFeatures(unusable=1.0, thin_look=0.5)
    r = score_soup(bad, 9.5, "開店前")
    print("撮影不備/Brix9.5:", r.total_score, "usable=", r.usable)
