#!/usr/bin/env python3
"""Автономный WebRTC V3 через существующий codex-proxy, с журналом маршрутов.

Это разговор, а не агент Codex. Не запускает Desktop, CLI или app-server.
HTTPS/WSS принудительно идут через CONNECT; WebRTC UDP наблюдается отдельно.
"""
import argparse
import asyncio
import json
import math
import os
from pathlib import Path
import sys

import voice_core as voice
from routes import Journal, Network, Proxy, RouteError, tls_context

BaseSession = voice.VoiceSession


class ProxySession(BaseSession):
    async def start(self):
        try:
            await super().start()
            self.args.network.media_snapshot(self.pc)
        except BaseException:
            self.args.network.journal.add("voice_failed", phase=self.phase)
            raise

    async def stats(self):
        stats = await super().stats()
        self.args.network.journal.add("rtp_stats", **stats)
        self.args.network.media_snapshot(self.pc)
        self.recorded_stats = True
        return stats

    async def close(self):
        if self.closed:
            return
        try:
            # Консоль тоже фиксирует RTP перед закрытием, не только файловая проба.
            if self.pc.connectionState == "connected" and not getattr(self, "recorded_stats", False):
                try:
                    await asyncio.wait_for(self.stats(), 2)
                except Exception:
                    self.args.network.journal.add("rtp_stats_unavailable")
            await super().close()
        finally:
            # Защита вокруг замороженного core: сбой speaker.abort/WS.close
            # не должен прервать освобождение остальных ресурсов. Это важно
            # также для Ctrl+C в ожидании статистики. Ошибки без str(error).
            self.closed = True
            try:
                self.track.stop()
            except Exception:
                self.args.network.journal.add("capture_close_failed")
            for task in self.tasks:
                task.cancel()
            await asyncio.gather(*self.tasks, return_exceptions=True)
            await self.pc.close()
            if self.speaker is not None:
                speaker, self.speaker = self.speaker, None
                for operation in (speaker.abort, speaker.close):
                    try:
                        operation()
                    except Exception:
                        self.args.network.journal.add("speaker_close_failed")
            if self.ws is not None:
                try:
                    await asyncio.wait_for(self.ws.close(), 2)
                except Exception:
                    self.args.network.journal.add("sideband_close_failed")
            if self.http is not None:
                await self.http.close()
            self.finished.set()


# Снимок core использует эту фабрику и в file_probe, и в консоли.
# Подмена ограничена данным Python-процессом; исходный direct demo не трогаем.
voice.VoiceSession = ProxySession


async def run(args, pcm, journal):
    proxy = Proxy.from_env(args.proxy_env)
    context = tls_context(args.ca_file)
    args.network = Network(proxy, context, journal, args.timeout, args.sideband_route)
    journal.add("run_started", proxy=proxy.public(), sideband_route=args.sideband_route,
                media_route="not_yet_observed", credentials_source="auth_file_read_only",
                no_proxy_ignored=True, file_probe=pcm is not None)
    journal.install_udp_audit()
    try:
        await args.network.relay.start()
        if pcm is not None:
            await voice.file_probe(args, pcm)
        else:
            await voice.console(args)
    finally:
        await args.network.relay.close()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--auth-file", type=Path, default=Path.home() / ".codex/auth.json",
                        help="Собственный ChatGPT вход; не acodex marker-профиль")
    parser.add_argument("--proxy-env", default="HTTPS_PROXY", help="Имя переменной, не URL с паролем")
    parser.add_argument("--ca-file", type=Path, default=Path(os.getenv("CODEX_CA_CERTIFICATE") or
                        os.getenv("SSL_CERT_FILE") or str(Path.home() / ".codex/ca-bundle.pem")))
    parser.add_argument("--report", type=Path, help="Новый закрытый JSON; создаётся также при отказе")
    parser.add_argument("--sideband-route", choices=["proxy", "direct"], default="proxy",
                        help="direct только явно для сравнения; автоматического fallback нет")
    parser.add_argument("--voice", choices=voice.VOICES, default="cove")
    parser.add_argument("--model")
    parser.add_argument("--input-device", type=int)
    parser.add_argument("--output-device", type=int)
    parser.add_argument("--file", type=Path, help="WAV 48 кГц mono PCM16 вместо микрофона")
    parser.add_argument("--output", type=Path, help="Новый WAV ответа, только с --file")
    parser.add_argument("--timeout", type=float, default=30)
    parser.add_argument("--max-seconds", type=float, default=45)
    parser.add_argument("--list-voices", action="store_true")
    parser.add_argument("--list-devices", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    for name in ("auth_file", "ca_file", "report", "file", "output"):
        if getattr(args, name) is not None:
            setattr(args, name, getattr(args, name).expanduser())
    if args.list_voices:
        print("V3 / CLI 0.156.1: " + ", ".join(voice.VOICES))
        return 0
    if args.list_devices:
        import sounddevice as sd
        print(sd.query_devices())
        return 0
    for value in (args.timeout, args.max_seconds):
        if not math.isfinite(value) or not 0 < value <= 300:
            parser.error("timeout/max-seconds: конечное число 0 < N <= 300")
    if bool(args.file) != bool(args.output):
        parser.error("--file и --output задаются вместе")
    if args.output and args.output.exists():
        parser.error("--output уже существует")
    if args.report and args.report.exists():
        parser.error("--report уже существует")
    if args.output and args.report and args.output.absolute() == args.report.absolute():
        parser.error("--report и --output должны различаться")
    if not args.proxy_env.isidentifier():
        parser.error("--proxy-env должен содержать имя переменной")
    if args.dry_run:
        print(json.dumps({"network": False, "credentials_read": False, "microphone": False,
                          "codex_process": False, "call": {"url": voice.CALL_URL, "route": "proxy"},
                          "sideband": {"url": voice.SIDEBAND + "<call_id>", "route": args.sideband_route},
                          "media": "WebRTC Opus DTLS-SRTP over UDP; outside HTTP CONNECT proxy",
                          "voice": args.voice}, ensure_ascii=False))
        return 0
    if not args.report:
        parser.error("Нужен --report для наблюдения фактического маршрута")
    journal = Journal()
    result = {"status": "failed"}
    code = 1
    try:
        # Некорректный файл отказываем ДО чтения credentials и до сети.
        pcm = voice.read_wav(args.file) if args.file else None
        asyncio.run(run(args, pcm, journal))
        code = 0
        result = {"status": "file_probe_success" if pcm is not None else "console_closed",
                  "sideband_route": args.sideband_route}
    except KeyboardInterrupt:
        result = {"status": "interrupted"}
        code = 130
    except Exception as error:
        # Только собственные контролируемые сообщения. aiohttp exceptions могут
        # содержать заголовки Authorization, поэтому str для них не используем.
        safe = str(error) if isinstance(error, (RouteError, voice.VoiceError)) else type(error).__name__
        result = {"status": "failed", "error": safe}
        print(safe, file=sys.stderr)
    finally:
        try:
            journal.save(args.report, result)
            print("Журнал маршрутов: " + str(args.report))
        except Exception:
            print("Не удалось создать новый закрытый --report", file=sys.stderr)
            code = 1
    return code


if __name__ == "__main__":
    raise SystemExit(main())
