"""
recorder.py — 録音専用モジュール (live_session/v2)
役割: ffmpeg (avfoundation) で30秒チャンク wav を recording/raw/ に書き出すだけ。
文字起こしは transcriber.py が担当。単一責務厳守。

自己増殖防止のため:
  - 出力ディレクトリ(recording/raw/)を入力が来るディレクトリから物理分離
  - ファイルパターン除外条件を多重起動チェックに組み込み
  - 多重起動拒否 (pgrep で自分以外の recorder.py を検出したら即終了)
を実装。
"""

import argparse
import logging
import os
import signal
import subprocess
import sys
from pathlib import Path
from typing import Optional

# ─── パス定義 ──────────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent
RAW_DIR = BASE_DIR / "recording" / "raw"
LOG_DIR = BASE_DIR / "logs"
LOG_FILE = LOG_DIR / "recorder.log"

# ─── 安全装置定数 ──────────────────────────────────────────────
DISK_LIMIT_BYTES = 1 * 1024 ** 3   # 1 GB
FILE_COUNT_LIMIT = 200              # 30秒 × 200 = 100分


# ─── ロガー初期化 ──────────────────────────────────────────────
def setup_logger() -> logging.Logger:
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    logger = logging.getLogger("recorder")
    logger.setLevel(logging.DEBUG)
    fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")

    sh = logging.StreamHandler(sys.stdout)
    sh.setFormatter(fmt)
    logger.addHandler(sh)

    fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
    fh.setFormatter(fmt)
    logger.addHandler(fh)

    return logger


logger = setup_logger()


# ─── 安全装置A: ディスク使用量監視 ────────────────────────────
def check_disk_usage(raw_dir: Path) -> bool:
    """raw_dir 内の合計サイズが DISK_LIMIT_BYTES を超えたら False を返す。"""
    if not raw_dir.exists():
        return True
    total = sum(f.stat().st_size for f in raw_dir.glob("*.wav"))
    if total > DISK_LIMIT_BYTES:
        logger.error(
            "[safety] ディスク使用量が上限(1GB)を超えました (%.1f MB)。録音停止。",
            total / 1024 ** 2,
        )
        return False
    return True


# ─── 安全装置B: ファイル数監視 ─────────────────────────────────
def check_file_count(raw_dir: Path) -> bool:
    """raw_dir 内の wav 数が FILE_COUNT_LIMIT を超えたら False を返す。"""
    if not raw_dir.exists():
        return True
    count = len(list(raw_dir.glob("*.wav")))
    if count > FILE_COUNT_LIMIT:
        logger.error(
            "[safety] チャンクファイル数が上限(%d)を超えました (%d個)。録音停止。",
            FILE_COUNT_LIMIT,
            count,
        )
        return False
    return True


# ─── 安全装置C: 多重起動チェック ──────────────────────────────
def check_no_duplicate_recorder() -> bool:
    """
    自分以外の python が recorder.py を実行中なら False を返す。
    pgrep -f だと bash スクリプト中の "recorder.py" 文字列でも誤ヒットするため、
    ps -ax で「python が recorder.py を実行している」プロセスのみ抽出する。
    """
    my_pid = os.getpid()
    my_pid_str = str(my_pid)
    try:
        result = subprocess.run(
            ["ps", "-ax", "-o", "pid,command"],
            capture_output=True, text=True,
        )
        others = []
        for line in result.stdout.splitlines()[1:]:
            line = line.strip()
            if not line:
                continue
            parts = line.split(None, 1)
            if len(parts) < 2:
                continue
            pid_str, cmd = parts
            if pid_str == my_pid_str:
                continue
            if "recorder.py" in cmd and ("python" in cmd.lower()):
                try:
                    others.append(int(pid_str))
                except ValueError:
                    continue
        if others:
            logger.error(
                "[safety] 別の recorder.py が既に起動中です (PID: %s)。多重起動を拒否します。",
                others,
            )
            return False
    except Exception as e:
        logger.warning("[safety] 多重起動チェックに失敗しました: %s", e)
    return True


# ─── 安全装置D: ffmpeg 既存プロセスチェック ───────────────────
def check_no_duplicate_ffmpeg(device_name: str) -> bool:
    """
    avfoundation でマイクを掴んでいる ffmpeg が既に動いていたら False を返す。
    """
    try:
        result = subprocess.run(
            ["pgrep", "-f", f"avfoundation.*{device_name}"],
            capture_output=True,
            text=True,
        )
        pids = [p for p in result.stdout.strip().split() if p]
        if pids:
            logger.error(
                "[safety] デバイス '%s' を使用中の ffmpeg が既に起動中です (PID: %s)。起動を拒否します。",
                device_name,
                pids,
            )
            return False
    except Exception as e:
        logger.warning("[safety] ffmpeg 多重起動チェックに失敗しました: %s", e)
    return True


# ─── ffmpeg コマンド構築 ────────────────────────────────────────
def build_ffmpeg_cmd(
    device_name: Optional[str],
    device_index: Optional[int],
    chunk_sec: int,
    raw_dir: Path,
) -> list:
    """
    avfoundation で録音し、chunk_sec 秒ごとに CHUNK_NNN.wav を出力するコマンド。
    出力先は raw_dir/CHUNK_%03d.wav 固定 (入力ディレクトリとは完全に別)。
    """
    if device_index is not None:
        input_spec = f":{device_index}"
    else:
        name = device_name or "Wireless Microphone"
        input_spec = f":{name}"

    output_pattern = str(raw_dir / "CHUNK_%03d.wav")

    cmd = [
        "ffmpeg",
        "-f", "avfoundation",
        "-i", input_spec,
        "-ar", "16000",       # 16kHz (Whisper 推奨)
        "-ac", "1",           # モノラル
        "-f", "segment",
        "-segment_time", str(chunk_sec),
        "-segment_start_number", "0",
        "-reset_timestamps", "1",
        output_pattern,
    ]
    return cmd


# ─── graceful stop ─────────────────────────────────────────────
_ffmpeg_proc: Optional[subprocess.Popen] = None


def _signal_handler(signum: int, frame) -> None:
    logger.info("[recorder] シグナル %s 受信。ffmpeg に SIGINT を送信して停止します。", signum)
    if _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None:
        _ffmpeg_proc.send_signal(signal.SIGINT)


# ─── メイン録音ループ ──────────────────────────────────────────
def run_recorder(
    device_name: Optional[str],
    device_index: Optional[int],
    chunk_sec: int,
) -> int:
    global _ffmpeg_proc

    RAW_DIR.mkdir(parents=True, exist_ok=True)
    LOG_DIR.mkdir(parents=True, exist_ok=True)

    # 起動前安全チェック
    if not check_no_duplicate_recorder():
        return 1
    if not check_no_duplicate_ffmpeg(device_name or "Wireless Microphone"):
        return 1
    if not check_disk_usage(RAW_DIR):
        return 1
    if not check_file_count(RAW_DIR):
        return 1

    cmd = build_ffmpeg_cmd(device_name, device_index, chunk_sec, RAW_DIR)
    logger.info("[recorder] 録音開始。コマンド: %s", " ".join(cmd))
    logger.info("[recorder] 出力先: %s", RAW_DIR)

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

    _ffmpeg_proc = subprocess.Popen(cmd)

    while True:
        ret = _ffmpeg_proc.poll()
        if ret is not None:
            logger.info("[recorder] ffmpeg 終了 (returncode=%d)。", ret)
            break

        # ループごとにディスク/ファイル数チェック
        if not check_disk_usage(RAW_DIR) or not check_file_count(RAW_DIR):
            _ffmpeg_proc.send_signal(signal.SIGINT)
            _ffmpeg_proc.wait()
            return 1

        try:
            _ffmpeg_proc.wait(timeout=5)
            break
        except subprocess.TimeoutExpired:
            pass  # 正常に録音継続中

    logger.info("[recorder] 録音停止。")
    return 0


# ─── dry-run ───────────────────────────────────────────────────
def run_dry_run(
    device_name: Optional[str],
    device_index: Optional[int],
    chunk_sec: int,
) -> int:
    print("[recorder] dry-run 開始")
    all_ok = True

    # 1. 出力先パス確認
    try:
        RAW_DIR.mkdir(parents=True, exist_ok=True)
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        print(f"  [OK] 出力先パス作成/確認: {RAW_DIR}")
    except Exception as e:
        print(f"  [NG] 出力先パス作成失敗: {e}")
        all_ok = False

    # 2. ffmpeg 動作確認
    try:
        result = subprocess.run(
            ["ffmpeg", "-version"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode == 0:
            first_line = result.stdout.splitlines()[0] if result.stdout else "(no output)"
            print(f"  [OK] ffmpeg 検出: {first_line}")
        else:
            print(f"  [NG] ffmpeg -version が失敗しました (rc={result.returncode})")
            all_ok = False
    except FileNotFoundError:
        print("  [NG] ffmpeg が見つかりません (PATH に追加してください)")
        all_ok = False
    except Exception as e:
        print(f"  [NG] ffmpeg 確認エラー: {e}")
        all_ok = False

    # 3. 多重起動チェック関数動作確認
    my_pid = os.getpid()
    try:
        result = subprocess.run(
            ["pgrep", "-f", "recorder.py"],
            capture_output=True,
            text=True,
        )
        pids = [int(p) for p in result.stdout.strip().split() if p]
        others = [p for p in pids if p != my_pid]
        if others:
            print(f"  [WARN] 別の recorder.py が検出されました (PID: {others})。本番では起動拒否します。")
        else:
            print(f"  [OK] 多重起動チェック: 自分(PID={my_pid})のみ / 他プロセスなし")
    except Exception as e:
        print(f"  [NG] 多重起動チェックエラー: {e}")
        all_ok = False

    # 4. ディスク監視関数動作確認 (閾値を1バイトにして発火チェック)
    try:
        # 仮の極小閾値でチェック関数ロジックを確認
        wav_files = list(RAW_DIR.glob("*.wav")) if RAW_DIR.exists() else []
        total = sum(f.stat().st_size for f in wav_files)
        tiny_limit = 1  # 意図的に1バイトで発火させてロジック確認
        fired = total > tiny_limit
        print(
            f"  [OK] ディスク監視関数: 現在 {total} bytes / 仮閾値 {tiny_limit} bytes → "
            f"{'発火 (正常動作確認)' if fired else '未発火 (ファイルなし=正常)'}"
        )
    except Exception as e:
        print(f"  [NG] ディスク監視チェックエラー: {e}")
        all_ok = False

    # 5. ffmpeg コマンド構築確認
    cmd = build_ffmpeg_cmd(device_name, device_index, chunk_sec, RAW_DIR)
    print(f"  [OK] ffmpeg コマンド構築: {' '.join(cmd)}")

    if all_ok:
        print("[recorder] dry-run: 全項目OK")
        return 0
    else:
        print("[recorder] dry-run: 一部項目NG。上記を確認してください。")
        return 1


# ─── エントリポイント ──────────────────────────────────────────
def main() -> None:
    parser = argparse.ArgumentParser(description="録音専用モジュール (live_session/v2)")
    parser.add_argument("--dry-run", action="store_true", help="設定確認のみ・録音なし")
    parser.add_argument(
        "--chunk-sec",
        type=int,
        default=int(os.environ.get("CHUNK_SEC", "30")),
        help="チャンク長(秒) [default: 30]",
    )
    parser.add_argument(
        "--device-name",
        type=str,
        default=os.environ.get("DEVICE_NAME", "Wireless Microphone"),
        help="avfoundation デバイス名",
    )
    parser.add_argument(
        "--device-index",
        type=int,
        default=int(os.environ["DEVICE_INDEX"]) if "DEVICE_INDEX" in os.environ else None,
        help="avfoundation デバイスインデックス (指定時は device-name より優先)",
    )
    args = parser.parse_args()

    if args.dry_run:
        sys.exit(run_dry_run(args.device_name, args.device_index, args.chunk_sec))
    else:
        sys.exit(run_recorder(args.device_name, args.device_index, args.chunk_sec))


if __name__ == "__main__":
    main()

# ─── 自己増殖防止メモ ──────────────────────────────────────────
# 2026-05-04 事故 (whisper-cli 26並列爆死) の再発防止として以下を実装:
#   1. 出力 dir 分離: recording/raw/ (本モジュール) と transcribed/ (transcriber.py) を
#      物理的に分けることで出力ファイルが入力として再帰的に食われるバグを根絶
#   2. ファイルパターン除外: 多重起動チェックで自 PID を除外し誤検知なし
#   3. 多重起動拒否: pgrep で recorder.py / ffmpeg の二重チェックを起動前に必ず実行
