#!/usr/bin/env python3
"""
stream_transcriber.py — MARK Ⅶ Phase 2 ストリームモジュール

whisper-stream を subprocess.Popen で常駐起動し、
stdout を 1行ずつ読んで live_transcript.txt に追記する。

中間ファイル不要・追記専用ファイル・多重起動拒否・whisper-stream子プロセス1個ガード
"""

import argparse
import os
import re
import signal
import subprocess
import sys
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

# ── パス定数 ─────────────────────────────────────────────────────────────────
_V3_DIR = Path(__file__).parent.resolve()
_LOG_DIR = _V3_DIR / "logs"
_TRANSCRIPT_FILE = _V3_DIR / "live_transcript.txt"
_LOG_FILE = _LOG_DIR / "stream_transcriber.log"
_WHISPER_BIN = "/opt/homebrew/bin/whisper-stream"
_DEFAULT_MODEL = (
    "/Users/rk/Library/CloudStorage/GoogleDrive-ritsuha1021@gmail.com"
    "/マイドライブ/起業フォルダー/clone-jarvis/jarvis/live_session/models/ggml-small.bin"
)
_MAX_TRANSCRIPT_BYTES = 100 * 1024 * 1024  # 100 MB

# ── JST タイムゾーン ──────────────────────────────────────────────────────────
_JST = timezone(timedelta(hours=9))


def _now_iso() -> str:
    """現在時刻を ISO 8601 (JST) で返す"""
    return datetime.now(_JST).strftime("%Y-%m-%dT%H:%M:%S+09:00")


def _now_compact() -> str:
    """ログファイル名用コンパクト形式"""
    return datetime.now(_JST).strftime("%Y%m%d_%H%M%S")


# ── ハルシネーションフィルタ ──────────────────────────────────────────────────
# whisper の雑音時ハルシネーション定番パターン
_HALLUCINATION_PATTERNS = [
    re.compile(r"^\s*\([^)]*\)\s*$"),           # (音楽) (拍手) 等の括弧のみ行
    re.compile(r"^\s*(?:\([^)]*\)\s*){2,}$"),  # (ポッ)(パンッ)(ポッ)... 連続効果音行
    re.compile(r"^\s*[ンん]+[\s?？!！]*$"),      # 「ンー」「んん」「ん？」等の沈黙ハルシネーション
    re.compile(r"[♪♫♬♩]"),                        # 音符記号
    re.compile(r"ご視聴ありがとうございました"),   # YouTube系
    re.compile(r"ご清聴ありがとうございました"),
    re.compile(r"字幕は自動的に生成されました"),
    re.compile(r"字幕翻訳"),
    re.compile(r"翻訳\s*:\s*"),
    re.compile(r"^[\s\.\-_=~・…]+$"),             # 記号のみ行
    re.compile(r"^\s*$"),                         # 空行
    re.compile(r"\[BLANK_AUDIO\]"),
    re.compile(r"\[音楽\]"),
    re.compile(r"\[拍手\]"),
    re.compile(r"\[笑い\]"),
    re.compile(r"^\s*\[.*\]\s*$"),                # [any] 単独行（timestamp/info行）
    re.compile(r"^\s*>>"),                         # >> で始まる info 行
    re.compile(r"^\s*\d{2}:\d{2}:\d{2}"),         # 00:00:00 timestamp 行
    re.compile(r"whisper", re.IGNORECASE),         # whisper 内部 debug 行
    re.compile(r"^init:"),                         # init: ... 内部ログ
    re.compile(r"^main:"),                         # main: ... 内部ログ
    re.compile(r"^mel "),                          # mel ... 内部ログ
    re.compile(r"^system_info:"),
    re.compile(r"^sampling:"),
    re.compile(r"^log_mel_spectrogram"),
    re.compile(r"^ggml_"),
    re.compile(r"auto-detected"),
    re.compile(r"^processing"),
    re.compile(r"^load_backend"),                   # load_backend: loaded ... 内部ログ
    re.compile(r"^load_"),                           # load_metal/load_blas等
    re.compile(r"\[Start speaking\]"),               # whisper-stream 起動メッセージ
    re.compile(r"\[BLANK\]"),
    re.compile(r"\(音楽\)"),                         # （音楽）バリアント
    re.compile(r"^encoder\b"),
    re.compile(r"^decoder\b"),
    # 無音時の典型的ハルシネーション（whisper small モデル）
    re.compile(r"^さぁ、おねがい"),
    re.compile(r"^サイコン"),
    re.compile(r"^チャンネル登録"),
    re.compile(r"^Thanks for watching"),
    re.compile(r"^Thank you"),
    re.compile(r"^Bye"),
    re.compile(r"^See you"),
]

# 連続同一行ハルシネーション検出用（直近行と同じなら弾く）
_LAST_LINE_BUFFER = {"text": "", "count": 0}
_DUPE_THRESHOLD = 2  # 同じ内容が2回連続 → 以降弾く

# ANSIエスケープシーケンス除去用（whisper-stream は \r や [2K で行を上書きする）
_ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]|\[[0-9]+[A-Z]")


def _is_hallucination(line: str) -> bool:
    """ハルシネーション・ノイズ行なら True"""
    stripped = line.strip()
    if not stripped:
        return True
    for pat in _HALLUCINATION_PATTERNS:
        if pat.search(stripped):
            return True
    return False


# ── ロギング ──────────────────────────────────────────────────────────────────
_log_file_handle: Optional[object] = None


def _setup_logging() -> None:
    global _log_file_handle
    _LOG_DIR.mkdir(parents=True, exist_ok=True)
    _log_file_handle = open(_LOG_FILE, "a", encoding="utf-8", buffering=1)


def _log(msg: str) -> None:
    ts = _now_iso()
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    if _log_file_handle:
        try:
            _log_file_handle.write(line + "\n")
            _log_file_handle.flush()
        except Exception:
            pass


# ── 多重起動チェック ──────────────────────────────────────────────────────────
def _check_duplicate() -> bool:
    """
    自分以外に stream_transcriber.py が動いていれば True（多重起動）。
    watcher.py / recorder.py の厳格判定方式を踏襲。

    判定条件（全部満たす行のみ）:
      1. "stream_transcriber.py" を含む
      2. COMMAND 列がPython系バイナリで始まる（zsh -c ... のシェルラッパー行を除外）
      3. 自PID以外
    """
    my_pid = os.getpid()
    # Python 系バイナリの判定パターン（大文字小文字両対応）
    _python_cmd_re = re.compile(r"/[Pp]ython[\d.]*(?:\.app/Contents/MacOS/Python[\d.]*)?\s")
    try:
        result = subprocess.run(
            ["ps", "-eo", "pid,command"],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
        )
        lines = result.stdout.decode("utf-8", errors="replace").splitlines()
        for line in lines:
            if "stream_transcriber.py" not in line:
                continue
            # COMMAND 列を取り出す（1列目=pid、残り=command）
            parts = line.strip().split(None, 1)
            if len(parts) < 2:
                continue
            pid_str, command = parts[0], parts[1]
            # pid パース
            try:
                pid = int(pid_str)
            except ValueError:
                continue
            if pid == my_pid:
                continue
            # command が Python バイナリで直接起動されているか確認
            # zsh/bash -c ... のシェルラッパー行は除外
            cmd_lower = command.lower()
            if cmd_lower.startswith("python") or _python_cmd_re.match(command):
                # -c フラグで実行されているインラインスクリプトは除外
                # （python3 -c "...stream_transcriber.py..." 形式）
                if " -c " not in command and "\\ -c " not in command:
                    return True  # 自分以外が python で stream_transcriber.py を実行中
    except Exception:
        pass
    return False


# ── ファイルサイズ監視・ローテーション ────────────────────────────────────────
def _maybe_rotate(transcript_fh) -> object:
    """
    live_transcript.txt が 100 MB 超えたらローテーション。
    古いファイルを YYYYMMDD_HHMMSS 付きにリネームし、新規ハンドルを返す。
    """
    try:
        size = _TRANSCRIPT_FILE.stat().st_size
    except FileNotFoundError:
        return transcript_fh

    if size > _MAX_TRANSCRIPT_BYTES:
        _log(f"WARNING: live_transcript.txt が {size // 1024 // 1024} MB 超 → ローテーション開始")
        try:
            transcript_fh.flush()
            transcript_fh.close()
        except Exception:
            pass
        archive_name = _V3_DIR / f"live_transcript_{_now_compact()}.txt"
        try:
            _TRANSCRIPT_FILE.rename(archive_name)
            _log(f"ローテーション完了: {archive_name.name}")
        except Exception as e:
            _log(f"WARNING: ローテーション rename 失敗: {e}")
        # 新規ファイルを開く
        new_fh = open(_TRANSCRIPT_FILE, "a", encoding="utf-8", buffering=1)
        return new_fh
    return transcript_fh


# ── dry-run ───────────────────────────────────────────────────────────────────
def _dry_run(args) -> int:
    """dry-run: 全チェック項目を実行して結果を print する。全 OK なら 0 を返す。"""
    ok = True

    # 1. whisper-stream バイナリ存在確認
    ws_path = subprocess.run(
        ["which", "whisper-stream"],
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL,
    )
    if ws_path.returncode == 0:
        print(f"[OK] whisper-stream バイナリ: {ws_path.stdout.decode().strip()}")
    else:
        print(f"[NG] whisper-stream が見つからない")
        ok = False

    # 2. モデルファイル存在確認
    model_path = Path(args.model)
    if model_path.exists():
        print(f"[OK] モデルファイル: {model_path}")
    else:
        print(f"[NG] モデルファイルが存在しない: {model_path}")
        ok = False

    # 3. 出力ディレクトリ作成可能
    try:
        _V3_DIR.mkdir(parents=True, exist_ok=True)
        print(f"[OK] 出力ディレクトリ作成可能: {_V3_DIR}")
    except Exception as e:
        print(f"[NG] 出力ディレクトリ作成失敗: {e}")
        ok = False

    # 4. ログディレクトリ作成可能
    try:
        _LOG_DIR.mkdir(parents=True, exist_ok=True)
        print(f"[OK] ログディレクトリ作成可能: {_LOG_DIR}")
    except Exception as e:
        print(f"[NG] ログディレクトリ作成失敗: {e}")
        ok = False

    # 5. 多重起動チェック関数動作確認
    duplicate = _check_duplicate()
    if not duplicate:
        print("[OK] 多重起動チェック: 他プロセスなし（正常）")
    else:
        print("[NG] 多重起動チェック: 他に stream_transcriber.py が動いている")
        ok = False

    # 6. ハルシネーションフィルタ正規表現テスト
    test_cases = [
        ("ジャービス聞こえる？", False, "通常発話"),
        ("(音楽)", True, "括弧のみ行"),
        ("♪~", True, "音符記号"),
        ("ご視聴ありがとうございました", True, "YouTube系ハルシネーション"),
        ("", True, "空行"),
        ("  ", True, "空白のみ"),
        ("[BLANK_AUDIO]", True, "BLANK_AUDIO"),
        ("今日の会議を始めます", False, "通常発話2"),
    ]
    filter_ok = True
    for text, expect_filtered, desc in test_cases:
        result = _is_hallucination(text)
        if result == expect_filtered:
            status = "OK"
        else:
            status = "NG"
            filter_ok = False
            ok = False
        print(f"  [{status}] フィルタテスト「{desc}」: 入力={repr(text)}, "
              f"期待={'弾く' if expect_filtered else '通す'}, "
              f"結果={'弾いた' if result else '通した'}")
    if filter_ok:
        print("[OK] ハルシネーションフィルタ: 全テスト通過")
    else:
        print("[NG] ハルシネーションフィルタ: テスト失敗あり")

    print()
    if ok:
        print("==> 全項目 OK")
        return 0
    else:
        print("==> NG あり")
        return 1


# ── メインループ ──────────────────────────────────────────────────────────────
_child_proc: Optional[subprocess.Popen] = None
_transcript_fh = None
_stop_requested: bool = False  # グローバル：signal handler とメインループで共有


def _signal_handler(signum, frame) -> None:
    """SIGINT / SIGTERM: graceful shutdown"""
    global _stop_requested
    _stop_requested = True
    _log(f"シグナル受信 ({signum}) → graceful shutdown 開始")
    if _child_proc is not None:
        try:
            _child_proc.send_signal(signal.SIGINT)
        except Exception:
            pass


def _shutdown() -> None:
    global _child_proc, _transcript_fh, _log_file_handle
    if _child_proc is not None:
        try:
            _child_proc.send_signal(signal.SIGINT)
            _child_proc.wait(timeout=5)
            _log("whisper-stream 子プロセス停止完了")
        except Exception as e:
            _log(f"WARNING: 子プロセス停止時エラー: {e}")
            try:
                _child_proc.kill()
            except Exception:
                pass
        _child_proc = None
    if _transcript_fh is not None:
        try:
            _transcript_fh.flush()
            _transcript_fh.close()
        except Exception:
            pass
        _transcript_fh = None
    if _log_file_handle is not None:
        try:
            _log_file_handle.flush()
            _log_file_handle.close()
        except Exception:
            pass
        _log_file_handle = None


def _run(args) -> None:
    global _child_proc, _transcript_fh

    # 多重起動チェック
    if _check_duplicate():
        print("ERROR: 別の stream_transcriber.py が既に動いています。多重起動拒否。", file=sys.stderr)
        sys.exit(1)

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

    # 出力先準備
    _V3_DIR.mkdir(parents=True, exist_ok=True)
    _transcript_fh = open(_TRANSCRIPT_FILE, "a", encoding="utf-8", buffering=1)

    # whisper-stream コマンド構築
    cmd = [
        _WHISPER_BIN,
        "--step", str(args.step),
        "--length", str(args.length),
        "-m", args.model,
        "-l", "ja",
        "-c", str(args.device),
    ]

    # 自動再起動ループ（whisper-stream は無音時に勝手に終了することがある）
    restart_count = 0
    rotate_counter = 0

    while not _stop_requested:
        _log(f"起動 (試行{restart_count + 1}): {' '.join(cmd)}")

        # 子プロセス起動
        try:
            _child_proc = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                bufsize=0,
            )
        except Exception as e:
            _log(f"FATAL: whisper-stream 起動失敗: {e}")
            time.sleep(3)
            restart_count += 1
            if restart_count > 20:
                _log("再起動リトライ上限到達 → 諦める")
                break
            continue

        _log(f"whisper-stream PID={_child_proc.pid} 起動完了")
        if restart_count == 0:
            _log("ストリーム文字起こし開始（small モデルロード20秒級・[READY]待ち）")

        ready_marked = False
        try:
            for raw_line in _child_proc.stdout:
                # UTF-8 デコードエラー耐性
                try:
                    line = raw_line.decode("utf-8", errors="strict").rstrip("\n\r")
                except UnicodeDecodeError:
                    try:
                        line = raw_line.decode("utf-8", errors="replace").rstrip("\n\r")
                        _log(f"WARNING: UTF-8 デコードエラー（置換で継続）: {repr(line)}")
                    except Exception:
                        continue

                # ANSIエスケープ除去
                line = _ANSI_ESCAPE_RE.sub("", line)

                # [Start speaking] = whisper-stream モデルロード完了マーカー
                if not ready_marked and "Start speaking" in line:
                    _log("✅ READY: マイク待機開始・発話OK")
                    ready_marked = True

                # whisper-stream は \r で行を上書きする・1物理行に複数の認識結果が
                # 混ざる。\r で分割して各サブ行を個別判定する（2026-05-07修正）。
                sub_lines = [s.strip() for s in line.split("\r") if s.strip()]
                if not sub_lines:
                    continue

                for sub in sub_lines:
                    # ハルシネーション・ノイズフィルタ（サブ行単位）
                    if _is_hallucination(sub):
                        continue

                    # 連続同一行（同じ内容を2回以上）→ 以降弾く
                    if sub == _LAST_LINE_BUFFER["text"]:
                        _LAST_LINE_BUFFER["count"] += 1
                        if _LAST_LINE_BUFFER["count"] >= _DUPE_THRESHOLD:
                            continue
                    else:
                        _LAST_LINE_BUFFER["text"] = sub
                        _LAST_LINE_BUFFER["count"] = 1

                    # タイムスタンプ付きで追記
                    ts = _now_iso()
                    entry = f"[{ts}] {sub}\n"
                    try:
                        _transcript_fh.write(entry)
                        _transcript_fh.flush()
                    except Exception as e:
                        _log(f"WARNING: transcript 書き込みエラー: {e}")

                # 100 件ごとにサイズチェック・ローテーション判定
                rotate_counter += 1
                if rotate_counter >= 100:
                    rotate_counter = 0
                    _transcript_fh = _maybe_rotate(_transcript_fh)

        except Exception as e:
            _log(f"読み取りループ例外: {e}")

        # ─── ここまで来たら whisper-stream が終了している ───
        if _child_proc is not None:
            try:
                _child_proc.wait(timeout=2)
            except Exception:
                pass
            rc = _child_proc.returncode
            _log(f"whisper-stream 終了 (rc={rc})")
            _child_proc = None

        # SIGINT受信した場合は再起動しない
        if _stop_requested:
            break

        # 自動再起動：3秒待ってから再試行
        restart_count += 1
        _log(f"自動再起動準備 (3秒待機・通算{restart_count}回目)")
        time.sleep(3)

    _log("ストリーム読み取り終了（再起動ループ終了）")
    _shutdown()


# ── エントリポイント ──────────────────────────────────────────────────────────
def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="MARK Ⅶ Phase 2 — ストリーム文字起こしモジュール"
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="設定確認・チェックのみ実行して exit 0",
    )
    parser.add_argument(
        "--device",
        type=int,
        default=int(os.environ.get("WHISPER_DEVICE", "-1")),
        help="マイクデバイス番号 (-1=自動・whisper-streamが選択, 0=最初のマイク, デフォルト: -1)",
    )
    parser.add_argument(
        "--model",
        default=_DEFAULT_MODEL,
        help="ggml モデルファイルパス",
    )
    parser.add_argument(
        "--step",
        type=int,
        default=500,
        help="whisper-stream --step (ms, デフォルト 500)",
    )
    parser.add_argument(
        "--length",
        type=int,
        default=5000,
        help="whisper-stream --length (ms, デフォルト 5000)",
    )
    return parser.parse_args()


if __name__ == "__main__":
    args = _parse_args()
    if args.dry_run:
        sys.exit(_dry_run(args))
    else:
        _run(args)


# ═══════════════════════════════════════════════════════════════════════════════
# 自己増殖防止サマリ
# ───────────────────────────────────────────────────────────────────────────
# ・中間ファイル不要   : 録音chunkファイルを一切生成しない。
#                       whisper-stream がマイクから直接ストリームを処理する。
# ・追記専用ファイル   : live_transcript.txt は open("a") のみ。
#                       読み書き両用・上書き・新規作成は行わない。
# ・多重起動拒否       : 起動時に ps 判定で自分以外の stream_transcriber.py を
#                       検出したら即 sys.exit(1)。
# ・whisper-stream 1個ガード : 多重起動チェックを通過した時点で子プロセスは
#                       最大1個。Popen は1回のみ呼ぶ。
# ═══════════════════════════════════════════════════════════════════════════════
