#!/usr/bin/env python3
"""
Minimax music generation — deterministic CLI for hyper-animator skill.

Reads .env, constructs prompt from style/motion tags, calls Minimax API,
downloads the result. Exit 0 on success, 1 on failure (BGM skipped).

Usage:
    python3 scripts/minimax-gen.py \\
        --style "cyberpunk, dark electronic" \\
        --motion "glitch_cyber" \\
        --bpm 110 --duration 85 \\
        -o hyperframes-output/assets/bgm-full.mp3
"""
import argparse
import json
import os
import pathlib
import subprocess
import sys
import tempfile
import time
import urllib.request

# ── Tag-to-prompt mapping ──────────────────────────────────

STYLE_MAP = {
    "apple_like": "modern, clean, electronic",
    "cinematic": "cinematic, orchestral, dramatic",
    "cyberpunk": "cyberpunk, dark electronic, industrial",
    "minimal": "ambient, minimal, atmospheric",
    "social_dynamic": "upbeat, energetic, pop",
    "editorial": "jazz, sophisticated, smooth",
    "dark": "dark, brooding, bass-heavy",
    "playful": "playful, bouncy, light",
    "premium": "premium, polished, refined",
    "light": "light, airy, bright",
    "news": "newsroom, professional, driven",
    "retro": "retro, vintage, nostalgic",
}

MOTION_MAP = {
    "steady_premium": "80 BPM",
    "fast_impact": "140 BPM",
    "soft_fluid": "100 BPM",
    "glitch_cyber": "110 BPM, glitch, distorted",
}

# ── .env loading ────────────────────────────────────────────

def load_env():
    env_file = pathlib.Path.home() / ".claude" / "skills" / "hyper-animator" / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                k, v = line.split("=", 1)
                v = v.strip().strip('"').strip("'")
                if k.strip() not in os.environ:
                    os.environ[k.strip()] = v

# ── Silence trimming ─────────────────────────────────────────

def _trim_silence(path, threshold_db=-40):
    """Trim leading silence from audio file using ffmpeg (if available)."""
    try:
        tmp = path + ".trimmed.mp3"
        subprocess.run([
            "ffmpeg", "-y", "-i", path,
            "-af", f"silenceremove=start_periods=1:start_threshold={threshold_db}dB:start_silence=0.1",
            tmp
        ], capture_output=True, timeout=30)
        if os.path.exists(tmp) and os.path.getsize(tmp) > 1000:
            os.replace(tmp, path)
            print(f"  Trimmed leading silence from output")
        else:
            pass  # ffmpeg not available or trim produced empty output
    except Exception:
        pass  # Silently skip if ffmpeg unavailable


def _call_music_api(api_key, api_host, body_dict, timeout=180):
    """Call Minimax Music API via urllib.

    Music generation is synchronous and the response time scales with track
    length — a ~140s track can take well over 30s to generate. Use a 180s
    timeout so long tracks aren't cut off; a real timeout at 180s means the
    backend is degraded and retrying won't help.
    """
    import urllib.error as urlerror
    api_url = f"https://{api_host}/v1/music_generation"
    data = json.dumps(body_dict).encode('utf-8')
    req = urllib.request.Request(
        api_url, data=data,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json; charset=utf-8"
        },
        method="POST"
    )
    try:
        resp = urllib.request.urlopen(req, timeout=timeout)
        resp_text = resp.read().decode('utf-8')
        return (True, json.loads(resp_text))
    except urlerror.HTTPError as e:
        body = e.read().decode('utf-8')
        if body:
            return (True, json.loads(body))
        return (False, f"HTTP {e.code}")
    except Exception as e:
        return (False, str(e))


# ── Main ─────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Minimax music generation")
    parser.add_argument("--style", required=True, help="Comma-separated style tags")
    parser.add_argument("--motion", required=True, help="Comma-separated motion tags")
    parser.add_argument("--bpm", type=int, default=120, help="Target BPM")
    parser.add_argument("--duration", type=int, default=60, help="Target duration (seconds)")
    parser.add_argument("-o", "--output", required=True, help="Output file path (.mp3)")
    parser.add_argument("--dry-run", action="store_true", help="Validate config and print request JSON without calling API")
    parser.add_argument("--request-json", help="Write the request body to this JSON file (for audit), then exit")
    args = parser.parse_args()

    load_env()
    api_key = os.environ.get("MINIMAX_API_KEY", "")
    api_host = os.environ.get("MINIMAX_API_HOST", "api.minimaxi.com")

    if not api_key:
        print("MINIMAX_API_KEY not configured. Exit.")
        sys.exit(1)

    if args.dry_run:
        load_env()
        api_key = os.environ.get("MINIMAX_API_KEY", "")
        if not api_key:
            print("ERROR: MINIMAX_API_KEY not configured.", file=sys.stderr)
            sys.exit(1)

        # Build prompt (same as normal)
        style_tags = [s.strip() for s in args.style.split(",") if s.strip()]
        motion_tags = [m.strip() for m in args.motion.split(",") if m.strip()]
        keywords = []
        for tag in style_tags:
            if tag in STYLE_MAP: keywords.append(STYLE_MAP[tag])
        for tag in motion_tags:
            if tag in MOTION_MAP: keywords.append(MOTION_MAP[tag])
        if not any("BPM" in kw for kw in keywords):
            keywords.append(f"{args.bpm} BPM")
        keywords.append("pure instrumental, no vocals, background music")
        prompt = ", ".join(keywords)

        masked_key = api_key[:8] + "..." + api_key[-4:] if len(api_key) > 12 else "***"
        print(f"DRY RUN — no API call will be made")
        print(f"  Host: {api_host}")
        print(f"  Model: music-2.6")
        print(f"  Key: {masked_key}")
        print(f"  Prompt: {prompt}")
        print(f"  Request body:")
        print(json.dumps({
            "model": "music-2.6",
            "prompt": prompt,
            "is_instrumental": True,
            "lyrics_optimizer": False,
            "output_format": "url",
            "audio_setting": {"sample_rate": 44100, "bitrate": 256000, "format": "mp3"}
        }, indent=2))
        sys.exit(0)

    if args.request_json:
        load_env()
        style_tags = [s.strip() for s in args.style.split(",") if s.strip()]
        motion_tags = [m.strip() for m in args.motion.split(",") if m.strip()]
        keywords = []
        for tag in style_tags:
            if tag in STYLE_MAP: keywords.append(STYLE_MAP[tag])
        for tag in motion_tags:
            if tag in MOTION_MAP: keywords.append(MOTION_MAP[tag])
        if not any("BPM" in kw for kw in keywords):
            keywords.append(f"{args.bpm} BPM")
        keywords.append("pure instrumental, no vocals, background music")
        prompt = ", ".join(keywords)

        body = {
            "model": "music-2.6",
            "prompt": prompt,
            "is_instrumental": True,
            "lyrics_optimizer": False,
            "output_format": "url",
            "audio_setting": {"sample_rate": 44100, "bitrate": 256000, "format": "mp3"}
        }
        with open(args.request_json, 'w') as f:
            json.dump(body, f, indent=2, ensure_ascii=False)
        print(f"Request JSON written to {args.request_json}")
        sys.exit(0)

    # Build prompt from tags
    style_tags = [s.strip() for s in args.style.split(",") if s.strip()]
    motion_tags = [m.strip() for m in args.motion.split(",") if m.strip()]

    keywords = []
    for tag in style_tags:
        if tag in STYLE_MAP:
            keywords.append(STYLE_MAP[tag])
    for tag in motion_tags:
        if tag in MOTION_MAP:
            keywords.append(MOTION_MAP[tag])

    # BPM from args takes precedence
    if not any("BPM" in kw for kw in keywords):
        keywords.append(f"{args.bpm} BPM")
    keywords.append("pure instrumental, no vocals, background music")

    prompt = ", ".join(keywords)
    print(f"Prompt: {prompt}")

    # Build request body
    body_dict = {
        "model": "music-2.6",
        "prompt": prompt,
        "is_instrumental": True,
        "lyrics_optimizer": False,
        "output_format": "url",
        "audio_setting": {"sample_rate": 44100, "bitrate": 256000, "format": "mp3"}
    }

    MAX_RETRIES = 3  # Retry on transient API errors (2151, 1002, etc.)
    RETRY_DELAYS = [10, 20, 30]  # seconds, exponential-ish for transient errors

    print(f"  Calling Minimax API @ {api_host} (music-2.6, max 3 attempts)...")
    success = False
    for attempt in range(MAX_RETRIES):
        ok, data = _call_music_api(api_key, api_host, body_dict)

        if not ok:
            err_msg = str(data).lower()
            # Never retry on timeout — server is degraded
            if "timeout" in err_msg or "timed out" in err_msg:
                print(f"  Minimax: timed out — server may be degraded. BGM skipped.")
                break
            delay = RETRY_DELAYS[min(attempt, len(RETRY_DELAYS)-1)]
            print(f"  [{attempt+1}/{MAX_RETRIES}] Minimax: {data} — retry in {delay}s")
            time.sleep(delay)
            continue

        resp = data
        base = resp.get("base_resp", {})
        status = base.get("status_code", -1)
        status_msg = base.get("status_msg", "")

        # Success
        if status == 0:
            audio_url = resp["data"]["audio"]
            os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
            urllib.request.urlretrieve(audio_url, args.output)
            dur = resp.get("extra_info", {}).get("music_duration", 0)

            _trim_silence(args.output)

            print(f"Minimax OK: {dur/1000:.1f}s -> {args.output}")
            success = True
            break

        # Rate limit (1002) — always worth retrying
        if status == 1002:
            delay = RETRY_DELAYS[min(attempt, len(RETRY_DELAYS)-1)]
            print(f"  [{attempt+1}/{MAX_RETRIES}] Rate limited — retry in {delay}s")
            time.sleep(delay)
            continue

        # Server errors (>=2000) and transient errors (1001, 1033, 2151) — retry
        if status >= 2000 or status in (1001, 1033, 2151):
            delay = RETRY_DELAYS[min(attempt, len(RETRY_DELAYS)-1)]
            print(f"  [{attempt+1}/{MAX_RETRIES}] Minimax status={status} ({status_msg}) — retry in {delay}s")
            time.sleep(delay)
            continue

        # Auth/param errors — no point retrying
        if status in (1004, 2013, 2049):
            print(f"Minimax status={status} ({status_msg}) — not retrying (config error)")
            break

        # Unknown errors — retry
        delay = RETRY_DELAYS[min(attempt, len(RETRY_DELAYS)-1)]
        print(f"  [{attempt+1}/{MAX_RETRIES}] Minimax status={status} — retry in {delay}s")
        time.sleep(delay)

    if not success:
        error_info = {
            "source": "minimax-gen.py",
            "model": "music-2.6",
            "host": api_host,
            "style": args.style,
            "motion": args.motion,
            "bpm": args.bpm,
            "duration": args.duration,
            "attempts": MAX_RETRIES,
            "output_path": args.output,
        }
        print(json.dumps(error_info, indent=2, ensure_ascii=False), file=sys.stderr)
        print(f"Minimax failed after {MAX_RETRIES} attempt(s) — BGM skipped", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()
