#!/usr/bin/env python3
import json
import subprocess
import time
import urllib.request
from pathlib import Path

PROJECT = Path('/data/video-pipeline/Youtube-Video-Maker/Project/30052026-001-Mac-Dinh-Chi')
PROBE_ENDPOINT = 'http://192.168.1.104:8002/probe'
WORKER_HOST = '192.168.1.104'
WORKER_PASS = '11210119'
MAIN_VIDEO = PROJECT / 'output/main_story.mp4'
MAIN_PROBE = PROJECT / 'output/main_story_probe.json'
CONCAT_REPORT = PROJECT / 'output/concat_report.json'
PART_RANGES = PROJECT / 'parts/part_ranges.json'
QA_DIR = PROJECT / 'output/qa_main_story'
FRAMES_DIR = QA_DIR / 'frames'
REPORT = PROJECT / 'output/main_story_qa_report.json'
MANIFEST = PROJECT / 'logs/step16_qa_main_story_manifest.json'
DURATION_TOLERANCE = 0.75


def log(msg):
    print(time.strftime('%Y-%m-%d %H:%M:%S ') + msg, flush=True)


def post_json(url, payload, timeout=120):
    req = urllib.request.Request(
        url,
        data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
        headers={'Content-Type': 'application/json'},
        method='POST',
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode('utf-8'))


def probe(path):
    doc = post_json(PROBE_ENDPOINT, {'input_path': str(path)})
    fmt = doc.get('format', {})
    streams = doc.get('streams', [])
    video = [s for s in streams if s.get('codec_type') == 'video']
    audio = [s for s in streams if s.get('codec_type') == 'audio']
    return {
        'path': str(path),
        'duration_sec': float(fmt.get('duration') or 0),
        'format_name': fmt.get('format_name'),
        'size': int(fmt.get('size') or 0),
        'has_video': bool(video),
        'has_audio': bool(audio),
        'video_streams': video,
        'audio_streams': audio,
        'raw': doc,
    }


def worker(cmd, timeout=600):
    full = [
        'sshpass', '-p', WORKER_PASS, 'ssh',
        '-o', 'StrictHostKeyChecking=no',
        '-o', 'PreferredAuthentications=password',
        '-o', 'PubkeyAuthentication=no',
        f'root@{WORKER_HOST}', cmd,
    ]
    p = subprocess.run(full, capture_output=True, text=True, timeout=timeout)
    return {'returncode': p.returncode, 'stdout': p.stdout, 'stderr': p.stderr, 'cmd': cmd}


def extract_frame(ts, name):
    out = FRAMES_DIR / f'{name}_{ts:.3f}.jpg'
    cmd = (
        f"mkdir -p '{FRAMES_DIR}' && "
        f"ffmpeg -y -hide_banner -loglevel error -ss {ts:.3f} -i '{MAIN_VIDEO}' "
        f"-frames:v 1 -q:v 2 '{out}'"
    )
    res = worker(cmd, timeout=120)
    return {'label': name, 'timestamp_sec': round(ts, 3), 'frame_path': str(out), 'ok': out.exists(), 'worker': res}


def main():
    QA_DIR.mkdir(parents=True, exist_ok=True)
    FRAMES_DIR.mkdir(parents=True, exist_ok=True)
    MANIFEST.parent.mkdir(parents=True, exist_ok=True)
    if not MAIN_VIDEO.exists():
        raise FileNotFoundError(MAIN_VIDEO)
    concat = json.loads(CONCAT_REPORT.read_text(encoding='utf-8'))
    ranges = json.loads(PART_RANGES.read_text(encoding='utf-8'))['parts']
    media = probe(MAIN_VIDEO)
    target_total = sum(float(p['duration_sec']) for p in ranges)
    duration_diff = media['duration_sec'] - target_total

    duration = media['duration_sec']
    checkpoints = [
        ('early', min(30.0, max(1.0, duration * 0.08))),
        ('mid', duration * 0.5),
        ('late', max(1.0, duration - 45.0)),
    ]
    cumulative = 0.0
    joins = []
    for idx, part in enumerate(ranges[:-1], start=1):
        cumulative += float(part['duration_sec'])
        joins.append((f'join_{idx:02d}_{idx+1:02d}_before', max(0.0, cumulative - 0.25)))
        joins.append((f'join_{idx:02d}_{idx+1:02d}_after', min(duration - 0.1, cumulative + 0.25)))

    frames = [extract_frame(ts, name) for name, ts in checkpoints + joins]

    black_cmd = (
        f"ffmpeg -hide_banner -nostats -i '{MAIN_VIDEO}' "
        f"-vf blackdetect=d=0.30:pix_th=0.10 -an -f null - 2>&1 | tail -200"
    )
    silence_cmd = (
        f"ffmpeg -hide_banner -nostats -i '{MAIN_VIDEO}' "
        f"-af silencedetect=n=-45dB:d=0.80 -vn -f null - 2>&1 | tail -200"
    )
    black = worker(black_cmd, timeout=900)
    silence = worker(silence_cmd, timeout=900)
    black_events = [line for line in black['stdout'].splitlines() + black['stderr'].splitlines() if 'black_' in line]
    silence_events = [line for line in silence['stdout'].splitlines() + silence['stderr'].splitlines() if 'silence_' in line]

    checks = {
        'main_exists': MAIN_VIDEO.exists(),
        'has_audio': media['has_audio'],
        'has_video': media['has_video'],
        'duration_reasonable': abs(duration_diff) <= DURATION_TOLERANCE,
        'concat_completed': concat.get('status') == 'completed',
        'frames_extracted': all(f['ok'] for f in frames),
        'blackdetect_clear': len(black_events) == 0,
        # Some intentional pauses may appear, but long silences should be reviewed manually.
        'silencedetect_events_count': len(silence_events),
    }
    passed = all([
        checks['main_exists'], checks['has_audio'], checks['has_video'],
        checks['duration_reasonable'], checks['concat_completed'], checks['frames_extracted'],
        checks['blackdetect_clear'],
    ])
    report = {
        'project_id': PROJECT.name,
        'step': 16,
        'status': 'passed' if passed else 'needs_review',
        'passed': passed,
        'input_video': str(MAIN_VIDEO),
        'duration_sec': media['duration_sec'],
        'target_total_duration_sec': target_total,
        'duration_diff_sec': round(duration_diff, 3),
        'probe': media,
        'concat_report': str(CONCAT_REPORT),
        'checks': checks,
        'checkpoints': [{'label': n, 'timestamp_sec': round(t, 3)} for n, t in checkpoints],
        'part_joins': [{'label': n, 'timestamp_sec': round(t, 3)} for n, t in joins],
        'frames': [{k: v for k, v in f.items() if k != 'worker'} for f in frames],
        'blackdetect_events': black_events,
        'silencedetect_events': silence_events,
        'decision': 'Can proceed to intro/outro/BGM packaging only after human visual sync spot-check of extracted frames/video samples.' if passed else 'Do not proceed until QA findings are reviewed/fixed.',
        'manual_qa_required': [
            'Watch early/mid/late checkpoints and part joins to confirm voice/subtitle sync.',
            'Confirm transitions are smooth and no heading image holds under following scene narration.',
        ],
    }
    REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
    MANIFEST.write_text(json.dumps({
        'project_id': PROJECT.name,
        'step': 16,
        'status': report['status'],
        'qa_report': str(REPORT),
        'input_video': str(MAIN_VIDEO),
        'duration_sec': media['duration_sec'],
        'frames_dir': str(FRAMES_DIR),
    }, ensure_ascii=False, indent=2), encoding='utf-8')
    log('DONE ' + json.dumps({'status': report['status'], 'passed': passed, 'duration': media['duration_sec'], 'frames': len(frames), 'black_events': len(black_events), 'silence_events': len(silence_events)}, ensure_ascii=False))


if __name__ == '__main__':
    main()
