#!/usr/bin/env python3
"""
jarvis_active_daemon.py — MARK Ⅶ 能動型 daemon メインループ
全モジュール統合・1ループ try-except・トークン上限自動降格
"""

import argparse
import json
import os
import signal
import subprocess
import sys
import time
from datetime import datetime, date
from pathlib import Path
from typing import Optional, Tuple, List

# ─── パス定義 ────────────────────────────────────────────────
V3_DIR = Path(__file__).parent
TRANSCRIPT_PATH = V3_DIR / "live_transcript.txt"
STATE_PATH = V3_DIR / "daemon_state_v3.json"
TOKENS_PATH = V3_DIR / "tokens_v3.jsonl"
INTERVENTION_LOG = V3_DIR / "intervention_log.jsonl"
PID_FILE = V3_DIR / "daemon_v3.pid"
HEALTH_LOG = V3_DIR / "logs" / "health.jsonl"

# ─── 即時応答設定（2026-05-06 バグD対策） ─────────────────────
# wake_word filter 通過時、claude -p 判定を待たずに「はい」とローカル発話。
# ユーザー要求：「何でもいいから返事してくれ」=「聞こえた」シグナルの即時化。
IMMEDIATE_ACK_TEXT = "はい"  # 0.3〜0.5秒で完結する短文
IMMEDIATE_ACK_RATE = 280     # macOS say のレート（速め）

# ─── トークン上限（1日・モード別） ────────────────────────────
TOKEN_LIMITS_DAILY = {
    "SILENT": 30_000,
    "OBSERVING": 2_000_000,
    "ACTIVE": 5_000_000,
    "IMMEDIATE": 999_999_999,  # 制限なし
}

# ─── モジュール安全 import（失敗時フォールバック） ────────────

_import_warnings = []

# --- intervention_filter ---
# シグネチャ: evaluate_filter(transcript_60sec: str, last_speech_at: datetime,
#               current_mode: str = "OBSERVING", now: Optional[datetime] = None) -> dict
# 戻り値 dict: {"should_judge_full": bool, "trigger": str, ...}
try:
    from intervention_filter import evaluate_filter  # type: ignore
    _filter_ok = True
except ImportError as e:
    _import_warnings.append(f"[WARN] intervention_filter import失敗: {e} → ダミー使用")
    _filter_ok = False

    def evaluate_filter(transcript_60sec: str, last_speech_at: datetime,
                        current_mode: str = "OBSERVING",
                        now: Optional[datetime] = None) -> dict:
        return {"should_judge_full": False, "trigger": "none", "reason": "filter_unavailable"}

# --- intervention_judge ---
# シグネチャ（並走Agent実装中のため暫定）:
# judge_and_generate(buffer: str, mode: str, trigger: str) -> object
# 戻り値: .intervene bool, .response str, .tokens_used int, .duration_sec float
try:
    from intervention_judge import judge_and_generate  # type: ignore
    _judge_ok = True
except ImportError as e:
    _import_warnings.append(f"[WARN] intervention_judge import失敗: {e} → ダミー使用")
    _judge_ok = False

    class _DummyJudgement:
        intervene = False
        response = ""
        tokens_used = 0
        duration_sec = 0.0

    def judge_and_generate(buffer: str, mode: str, trigger: str) -> "_DummyJudgement":  # type: ignore
        return _DummyJudgement()

# --- mode_controller ---
# get_mode() -> Mode (enum)  .value で str 取得
# set_mode(mode_or_str) -> Mode
# detect_mode_switch_in_text(text: str) -> Optional[Mode]
# filter_threshold_for_mode(mode) -> set
try:
    from mode_controller import get_mode, set_mode, detect_mode_switch_in_text, filter_threshold_for_mode  # type: ignore
    _mode_ok = True
except ImportError as e:
    _import_warnings.append(f"[WARN] mode_controller import失敗: {e} → ダミー使用")
    _mode_ok = False

    def get_mode():  # type: ignore
        return type("Mode", (), {"value": "OBSERVING", "__str__": lambda s: "OBSERVING"})()

    def set_mode(mode) -> None:  # type: ignore
        pass

    def detect_mode_switch_in_text(text: str):  # type: ignore
        return None

    def filter_threshold_for_mode(mode) -> set:  # type: ignore
        return set()

# --- output_dispatcher ---
# dispatch(transcript, response, mode, trigger, tokens, duration) -> None
try:
    from output_dispatcher import dispatch  # type: ignore
    _dispatch_ok = True
except ImportError as e:
    _import_warnings.append(f"[WARN] output_dispatcher import失敗: {e} → ダミー使用")
    _dispatch_ok = False

    def dispatch(transcript: str, response: str, mode: str, trigger: str,  # type: ignore
                 tokens: int, duration: float) -> None:
        ts = datetime.now().strftime("%H:%M:%S")
        print(f"[active_daemon][{ts}] [dispatch fallback] mode={mode} trigger={trigger} response={response[:80]}")

# --- voice_output（即時応答用・dispatch とは独立経路） ---
try:
    import voice_output as _voice_output  # type: ignore
    _voice_ok = True
except ImportError as e:
    _import_warnings.append(f"[WARN] voice_output import失敗: {e} → 即時応答 OFF")
    _voice_ok = False
    _voice_output = None  # type: ignore


def speak_immediate_ack() -> None:
    """wake_word 検出直後の即時「聞こえた」シグナル。
    claude -p 経由しない・トークン消費 0・体感応答時間 0.3〜0.5 秒。
    失敗しても daemon メインループに影響させない（防御的に握りつぶす）。
    """
    if not _voice_ok or _voice_output is None:
        return
    try:
        _voice_output.speak(
            IMMEDIATE_ACK_TEXT,
            rate=IMMEDIATE_ACK_RATE,
            blocking=False,
        )
    except Exception as e:
        ts = datetime.now().strftime("%H:%M:%S")
        print(f"[active_daemon][{ts}] 即時応答 speak 失敗（無視）: {e}")


# ─── モード値ヘルパー ─────────────────────────────────────────

def mode_str(mode_obj) -> str:
    """Mode enum でも str でも .value か str() で文字列化"""
    if hasattr(mode_obj, "value"):
        return str(mode_obj.value)
    return str(mode_obj)


# ─── 状態管理 ─────────────────────────────────────────────────

def load_state() -> dict:
    if STATE_PATH.exists():
        try:
            return json.loads(STATE_PATH.read_text())
        except Exception:
            pass
    return {
        "last_offset": 0,
        "last_intervention_at": 0.0,
        "started_at": time.time(),
    }

def save_state(state: dict) -> None:
    STATE_PATH.write_text(json.dumps(state, indent=2))

# ─── トークン累計 ─────────────────────────────────────────────

def append_token_record(mode: str, tokens: int) -> None:
    record = {"ts": datetime.now().isoformat(), "mode": mode, "tokens": tokens}
    with open(TOKENS_PATH, "a") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")


def append_health_record(record: dict) -> None:
    """health.jsonl に 1 行追記。バグC（時間経過で反応しなくなる）真因特定用ログ。"""
    record.setdefault("ts", datetime.now().isoformat())
    try:
        HEALTH_LOG.parent.mkdir(parents=True, exist_ok=True)
        with open(HEALTH_LOG, "a", encoding="utf-8") as f:
            f.write(json.dumps(record, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _whisper_stream_alive() -> bool:
    """whisper-stream プロセスが現在生きているか。
    health 計測専用・kill 等の制御はしない（観測のみ）。"""
    try:
        result = subprocess.run(
            ["pgrep", "-f", "whisper-stream"],
            capture_output=True, text=True, timeout=2
        )
        return bool(result.stdout.strip())
    except Exception:
        return False

def get_daily_tokens(mode: str) -> int:
    today = date.today().isoformat()
    total = 0
    if not TOKENS_PATH.exists():
        return 0
    with open(TOKENS_PATH) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                r = json.loads(line)
                if r.get("ts", "").startswith(today) and r.get("mode") == mode:
                    total += r.get("tokens", 0)
            except Exception:
                pass
    return total

def check_token_limit(mode: str) -> bool:
    """上限超えなら True を返す"""
    limit = TOKEN_LIMITS_DAILY.get(mode, 999_999_999)
    if limit >= 999_999_999:
        return False
    used = get_daily_tokens(mode)
    return used >= limit

# ─── transcript tail ──────────────────────────────────────────

def read_new_lines(state: dict) -> Tuple[List[str], int]:
    """新規行と新 offset を返す"""
    if not TRANSCRIPT_PATH.exists():
        return [], state.get("last_offset", 0)
    file_size = TRANSCRIPT_PATH.stat().st_size
    last_offset = state.get("last_offset", 0)
    # transcript ローテーション/クリア検出：
    # state の last_offset が現ファイルサイズより大きければファイルが縮んだ
    # → 0 から読み直す（2026-05-07 修正：transcript クリア後に
    #   daemon が永遠に新規行を読めない問題への対策）
    if last_offset > file_size:
        last_offset = 0
    with open(TRANSCRIPT_PATH, "r", encoding="utf-8", errors="replace") as f:
        f.seek(last_offset)
        new_text = f.read()
        new_offset = f.tell()
    lines = [l for l in new_text.splitlines() if l.strip()]
    return lines, new_offset

def update_buffer(buffer_lines: list, new_lines: List[str],
                  window_sec: int = 60) -> Tuple[list, datetime]:
    """直近 window_sec 秒のバッファ更新。last_speech_at (datetime) を返す"""
    now_ts = time.time()
    for l in new_lines:
        buffer_lines.append((now_ts, l))
    cutoff = now_ts - window_sec
    buffer_lines = [(t, l) for t, l in buffer_lines if t >= cutoff]
    if buffer_lines:
        last_ts = buffer_lines[-1][0]
        last_speech_at = datetime.fromtimestamp(last_ts)
    else:
        last_speech_at = datetime.fromtimestamp(0)
    return buffer_lines, last_speech_at

def buffer_text(buffer_lines: list) -> str:
    return "\n".join(l for _, l in buffer_lines)

# ─── 多重起動防止 ─────────────────────────────────────────────

def check_already_running() -> bool:
    if not PID_FILE.exists():
        return False
    try:
        pid = int(PID_FILE.read_text().strip())
        if pid == os.getpid():
            return False
        import subprocess
        result = subprocess.run(
            ["ps", "-p", str(pid), "-o", "pid=,comm="],
            capture_output=True, text=True
        )
        if result.returncode == 0 and "python" in result.stdout:
            return True
    except Exception:
        pass
    return False

def write_pid() -> None:
    PID_FILE.write_text(str(os.getpid()))

def remove_pid() -> None:
    try:
        PID_FILE.unlink(missing_ok=True)
    except Exception:
        pass

# ─── グレースフルシャットダウン ──────────────────────────────

_shutdown = False

def _signal_handler(sig, frame):
    global _shutdown
    ts = datetime.now().strftime("%H:%M:%S")
    print(f"\n[active_daemon][{ts}] シャットダウン信号受信 (sig={sig}) → graceful stop")
    _shutdown = True

signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)

# ─── ループ間隔定数 ───────────────────────────────────────────
LOOP_INTERVAL_SEC: float = 2.0   # メインループ sleep（1→2秒 MARK Ⅶ 軽量化 C-2）

# ─── dry-run ──────────────────────────────────────────────────

def run_dry_run() -> int:
    print("[active_daemon] dry-run 開始")

    # 1. import チェック
    for w in _import_warnings:
        print(w)

    # 2. transcript ファイルが無ければ touch
    if not TRANSCRIPT_PATH.exists():
        TRANSCRIPT_PATH.touch()
        print(f"[active_daemon] dry-run: {TRANSCRIPT_PATH.name} を touch")

    # 3. state 読み書き確認
    state = load_state()
    save_state(state)
    print(f"[active_daemon] dry-run: state OK (offset={state.get('last_offset', 0)})")

    # 4. get_mode 確認
    try:
        mode_obj = get_mode()
        mode = mode_str(mode_obj)
        print(f"[active_daemon] dry-run: get_mode() = {mode}")
    except Exception as e:
        print(f"[active_daemon] dry-run: get_mode() 失敗: {e}")
        mode = "OBSERVING"

    # 5. evaluate_filter 1回 (dummy buffer)
    try:
        result = evaluate_filter("dry-run test", datetime.now(), mode)
        sjf = result.get("should_judge_full", False) if isinstance(result, dict) else getattr(result, "should_judge_full", False)
        print(f"[active_daemon] dry-run: evaluate_filter() OK (should_judge_full={sjf})")
    except Exception as e:
        print(f"[active_daemon] dry-run: evaluate_filter() 失敗: {e}")

    # 6. 5秒メインループ（実 fire なし・dry_run=True で judge/dispatch はスキップ）
    print("[active_daemon] dry-run: 5秒メインループ開始")
    start = time.time()
    loop_count = 0
    buffer_lines: list = []
    last_speech_at: datetime = datetime.now()
    loop_timer = 0

    while time.time() - start < 5:
        loop_count += 1
        loop_timer += 1
        new_lines, _ = read_new_lines(state)
        buffer_lines, last_speech_at = update_buffer(buffer_lines, new_lines)

        if loop_timer >= 3:
            loop_timer = 0
            try:
                flt = evaluate_filter(buffer_text(buffer_lines), last_speech_at, mode)
                # dry-run: judge/dispatch は呼ばない
            except Exception as e:
                print(f"[active_daemon] dry-run filter error: {e}")

        time.sleep(LOOP_INTERVAL_SEC)

    print(f"[active_daemon] dry-run: {loop_count}ループ完走")
    print("[active_daemon] dry-run: 全項目OK")
    return 0

# ─── メインループ ─────────────────────────────────────────────

def run_main(initial_mode: Optional[str] = None) -> None:
    global _shutdown

    ts0 = datetime.now().strftime("%H:%M:%S")
    print(f"[active_daemon][{ts0}] 起動 pid={os.getpid()}")

    # import 警告出力
    for w in _import_warnings:
        print(w)

    # 多重起動チェック
    if check_already_running():
        print("[active_daemon] ERROR: すでに起動中。多重起動拒否。")
        sys.exit(1)
    write_pid()

    # transcript がなければ touch
    if not TRANSCRIPT_PATH.exists():
        TRANSCRIPT_PATH.touch()

    # state ロード
    state = load_state()
    state["started_at"] = time.time()
    save_state(state)

    # 初期モード設定
    if initial_mode:
        try:
            set_mode(initial_mode)
        except Exception as e:
            print(f"[active_daemon] モード設定失敗: {e}")

    buffer_lines: list = []
    last_speech_at: datetime = datetime.now()
    last_intervention_at = float(state.get("last_intervention_at", 0.0))
    loop_count = 0
    loop_timer = 0  # 5秒カウンタ
    health_timer = 0  # 30秒health計測カウンタ
    last_health_offset = 0
    # 重複介入防止（2026-05-06 改修）：
    # 旧 hash skip 方式は「30秒経過したら同一バッファでも再発火」の穴があり連発の原因に。
    # 新方式：「最後の介入時の transcript offset を記憶 → 新規発話が来るまで再発火しない」。
    # 介入後の追加発話（≒ offset 増加）があった時のみ次の介入を許可。
    # 加えて「最低クールダウン10秒」を併用し、書き込みノイズ等での連打を防ぐ。
    last_intervention_offset: int = int(state.get("last_intervention_offset", 0))
    INTERVENTION_MIN_INTERVAL_SEC: float = 10.0

    print("[active_daemon] メインループ開始")

    while not _shutdown:
        loop_start = time.time()
        loop_count += 1
        loop_timer += 1

        try:
            # 1. transcript 新規行読み込み
            new_lines, new_offset = read_new_lines(state)
            if new_offset != state.get("last_offset", 0):
                state["last_offset"] = new_offset
                save_state(state)

            # health 計測（30秒毎・バグC真因特定用）
            # ループ間隔2秒に合わせ 15ループ = 約30秒
            health_timer += 1
            if health_timer >= 15:
                health_timer = 0
                try:
                    transcript_size = TRANSCRIPT_PATH.stat().st_size if TRANSCRIPT_PATH.exists() else 0
                    growth_30s = transcript_size - last_health_offset
                    last_health_offset = transcript_size
                    append_health_record({
                        "transcript_size": transcript_size,
                        "growth_30s_bytes": growth_30s,
                        "whisper_stream_alive": _whisper_stream_alive(),
                        "buffer_lines_count": len(buffer_lines),
                        "loop_count": loop_count,
                        "last_intervention_offset": last_intervention_offset,
                    })
                except Exception:
                    pass

            # 2. バッファ更新（直近60秒）
            buffer_lines, last_speech_at = update_buffer(buffer_lines, new_lines)

            # 3. モード取得（auto-demote チェック込み）
            try:
                mode_obj = get_mode()
                mode = mode_str(mode_obj)
            except Exception:
                mode = "OBSERVING"

            # 4. transcript 内モード切替コマンド検出
            for _, line in buffer_lines:
                try:
                    new_mode_obj = detect_mode_switch_in_text(line)
                    if new_mode_obj is not None:
                        set_mode(new_mode_obj)
                        mode = mode_str(new_mode_obj)
                        ts = datetime.now().strftime("%H:%M:%S")
                        print(f"[active_daemon][{ts}] モード切替検出: {mode}")
                except Exception:
                    pass

            # 5. 約6秒に1回（2秒×3ループ）：フィルタ→判定→出力
            if loop_timer >= 3:
                loop_timer = 0

                # トークン上限チェック（降格）
                if check_token_limit(mode):
                    ts = datetime.now().strftime("%H:%M:%S")
                    print(f"[active_daemon][{ts}] トークン上限到達({mode}) → SILENT自動降格")
                    try:
                        set_mode("SILENT")
                        dispatch(
                            transcript="",
                            response=f"[Jarvis] トークン上限到達({mode})→SILENT降格",
                            mode="SILENT",
                            trigger="token_limit",
                            tokens=0,
                            duration=0.0,
                        )
                    except Exception as e:
                        print(f"[active_daemon] SILENT降格通知失敗: {e}")
                    mode = "SILENT"

                buf_text = buffer_text(buffer_lines)

                # 5a. 軽量フィルタ
                try:
                    flt = evaluate_filter(buf_text, last_speech_at, mode)
                    if not isinstance(flt, dict):
                        flt = {"should_judge_full": getattr(flt, "should_judge_full", False),
                               "trigger": getattr(flt, "trigger", "unknown")}
                except Exception as e:
                    ts = datetime.now().strftime("%H:%M:%S")
                    print(f"[active_daemon][{ts}] filter例外: {e}")
                    flt = {"should_judge_full": False, "trigger": "error"}

                # 5b. フィルタ通過 → 本格判定
                if flt.get("should_judge_full", False):
                    # filter は trigger_reason を返す・互換的に trigger も読む
                    trigger = flt.get("trigger_reason") or flt.get("trigger", "unknown")
                    matched_text = flt.get("matched_text", "")
                    now_ts = time.time()

                    # 即時応答（wake_word のみ・claude -p より先に発火）：
                    # ユーザーへ「聞こえた」シグナルを 0.3〜0.5 秒以内に返す。
                    # 連打防止より前にここで判定するのは、skip 判定で claude -p 発火しない
                    # 場合でも「聞いた」シグナルだけは欲しいから（仕様）。
                    if trigger == "wake_word":
                        speak_immediate_ack()

                    # silence trigger は廃止（2026-05-06 暴走事故対策）。
                    # 万一フィルタが silence を返しても無視する保険ガード。
                    if trigger == "silence":
                        continue

                    # 連打防止（2026-05-06 hash方式 → offset方式に置換）：
                    # 1) 最後の介入から INTERVENTION_MIN_INTERVAL_SEC 経過してない → skip
                    # 2) 最後の介入後に新規発話が無い（offset 不変） → skip
                    current_offset = state.get("last_offset", 0)
                    if (now_ts - last_intervention_at) < INTERVENTION_MIN_INTERVAL_SEC:
                        continue
                    if current_offset <= last_intervention_offset:
                        # 介入後に transcript が伸びてない＝同じ発話を再判定しているだけ
                        continue

                    ts = datetime.now().strftime("%H:%M:%S")
                    print(f"[active_daemon][{ts}] filter通過 trigger={trigger} → 本格判定")
                    try:
                        judgement = judge_and_generate(buf_text, trigger, matched_text)
                    except Exception as e:
                        ts = datetime.now().strftime("%H:%M:%S")
                        print(f"[active_daemon][{ts}] judge例外: {e}")
                        judgement = {"intervene": False, "comment": "", "tokens_estimated": 0, "duration_sec": 0.0}

                    # judgement は dict
                    if not isinstance(judgement, dict):
                        # 互換性 fallback
                        judgement = {
                            "intervene": getattr(judgement, "intervene", False),
                            "comment": getattr(judgement, "comment", "") or getattr(judgement, "response", ""),
                            "tokens_estimated": getattr(judgement, "tokens_used", 0),
                            "duration_sec": getattr(judgement, "duration_sec", 0.0),
                        }

                    # 5c. intervene=yes → 出力
                    if judgement.get("intervene", False):
                        tokens_used = judgement.get("tokens_estimated", 0)
                        duration = judgement.get("duration_sec", 0.0)
                        comment = judgement.get("comment", "")
                        ts = datetime.now().strftime("%H:%M:%S")
                        print(f"[active_daemon][{ts}] 介入: {comment[:40]} (tokens={tokens_used})")

                        try:
                            dispatch(
                                transcript_excerpt=buf_text,
                                jarvis_response=comment,
                                mode=mode,
                                trigger_reason=trigger,
                                tokens_used=tokens_used,
                                duration_sec=duration,
                            )
                        except Exception as e:
                            ts = datetime.now().strftime("%H:%M:%S")
                            print(f"[active_daemon][{ts}] dispatch例外: {e}")

                        # トークン記録
                        if tokens_used > 0:
                            append_token_record(mode, tokens_used)

                        last_intervention_at = time.time()
                        last_intervention_offset = state.get("last_offset", 0)
                        state["last_intervention_at"] = last_intervention_at
                        state["last_intervention_offset"] = last_intervention_offset
                        save_state(state)

                        # 介入後のbufferクリア（2026-05-06 バグC部分対策）：
                        # 介入で消費した発話を60秒window内に残し続けると、新規発話と混じって
                        # 古い「ジャービス」が再判定されたり filter が混乱する。介入完了時に
                        # in-memory buffer を空にして「次の発話」を新鮮な状態で受け取る。
                        # transcript ファイル本体は残す（ログ・解析用）。
                        buffer_lines = []

                        # IMMEDIATE モードなら OBSERVING に戻す
                        if mode == "IMMEDIATE":
                            try:
                                set_mode("OBSERVING")
                                ts = datetime.now().strftime("%H:%M:%S")
                                print(f"[active_daemon][{ts}] IMMEDIATE→OBSERVING 自動復帰")
                                mode = "OBSERVING"
                            except Exception as e:
                                print(f"[active_daemon] IMMEDIATE復帰失敗: {e}")

        except Exception as e:
            ts = datetime.now().strftime("%H:%M:%S")
            print(f"[active_daemon][{ts}] ループ例外（継続）: {e}")

        # スリープ（残時間補正・LOOP_INTERVAL_SEC=2秒）
        elapsed = time.time() - loop_start
        remaining = max(0.0, LOOP_INTERVAL_SEC - elapsed)
        if remaining > 0 and not _shutdown:
            time.sleep(remaining)

    # シャットダウン後処理
    save_state(state)
    remove_pid()
    ts = datetime.now().strftime("%H:%M:%S")
    print(f"[active_daemon][{ts}] 正常停止 (ループ回数={loop_count})")

# ─── エントリポイント ─────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(description="MARK Ⅶ 能動型 Jarvis daemon")
    parser.add_argument("--dry-run", action="store_true", help="モジュール確認・5秒テストループ後に終了")
    parser.add_argument("--mode", choices=["SILENT", "OBSERVING", "ACTIVE", "IMMEDIATE"],
                        default=None, help="起動時の初期モード")
    args = parser.parse_args()

    if args.dry_run:
        sys.exit(run_dry_run())
    else:
        run_main(initial_mode=args.mode)

if __name__ == "__main__":
    main()
