import json
import os
import shutil
import subprocess
import sys
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

import click
from tqdm import tqdm

GP_EXTENSIONS = {".gp", ".gpx", ".gp5", ".gp4", ".gp3", ".ptb"}

_MSCORE_MACOS_PATHS = [
    "/Applications/MuseScore 4.app/Contents/MacOS/mscore",
    "/Applications/MuseScore 3.app/Contents/MacOS/mscore",
]
_MSCORE_NAMES = ["mscore", "mscore4", "mscore3", "MuseScore"]

DEFAULT_BATCH_SIZE = 200


def _find_mscore(explicit=None):
    if explicit:
        return explicit
    for p in _MSCORE_MACOS_PATHS:
        if os.path.isfile(p):
            return p
    for name in _MSCORE_NAMES:
        found = shutil.which(name)
        if found:
            return found
    return None


def _run_json_batch(mscore_bin, jobs, log_fh):
    """Run one MuseScore -j batch. Returns (ok, failed) counts."""
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".json", delete=False, encoding="utf-8"
    ) as f:
        json.dump(jobs, f)
        job_file = f.name
    try:
        subprocess.run(
            [mscore_bin, "-F", "-f", "--gp-linked", "--gp-experimental", "-j", job_file],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=False,
        )
    finally:
        os.unlink(job_file)

    ok = failed = 0
    for job in jobs:
        if os.path.exists(job["out"]):
            ok += 1
        else:
            failed += 1
            msg = f"{job['in']}\tno output produced"
            if log_fh:
                log_fh.write(msg + "\n")
                log_fh.flush()
            else:
                click.echo(msg, err=True)
    return ok, failed


@click.command()
@click.argument("input_dir", type=click.Path(exists=True, file_okay=False))
@click.argument("output_dir", type=click.Path(file_okay=False))
@click.option("--workers", default=None, type=int, help="Parallel MuseScore processes (default: cpu count)")
@click.option("--batch-size", default=DEFAULT_BATCH_SIZE, show_default=True, type=int,
              help="Files per MuseScore process")
@click.option("--log", "log_path", default=None, type=click.Path(), help="Write errors to file instead of stderr")
@click.option("--mscore-path", default=None, type=click.Path(), help="Path to mscore binary")
@click.option("--resume", is_flag=True, default=False, help="Skip files whose output already exists")
def command(input_dir, output_dir, workers, batch_size, log_path, mscore_path, resume):
    """Convert Guitar Pro files to MusicXML using MuseScore (JSON batch mode)."""
    mscore_bin = _find_mscore(mscore_path)
    if not mscore_bin:
        raise click.ClickException(
            "MuseScore not found. Install MuseScore or pass --mscore-path."
        )

    input_dir = os.path.abspath(input_dir)
    output_dir = os.path.abspath(output_dir)
    os.makedirs(output_dir, exist_ok=True)

    files = [
        os.path.join(root, f)
        for root, _, fnames in os.walk(input_dir)
        for f in fnames
        if Path(f).suffix.lower() in GP_EXTENSIONS
    ]
    if not files:
        raise click.ClickException(f"No Guitar Pro files found in {input_dir}")

    def _out_path(fp):
        rel = os.path.relpath(os.path.dirname(fp), input_dir)
        return os.path.join(output_dir, rel, Path(fp).stem + ".musicxml")

    if resume:
        skipped = sum(1 for f in files if os.path.exists(_out_path(f)))
        files = [f for f in files if not os.path.exists(_out_path(f))]
        click.echo(f"Resuming: {skipped} already done, {len(files)} remaining.", err=True)

    if not files:
        click.echo("Nothing to do.", err=True)
        return

    for fp in files:
        os.makedirs(os.path.dirname(_out_path(fp)), exist_ok=True)

    jobs = [{"in": fp, "out": _out_path(fp)} for fp in files]
    batches = [jobs[i:i + batch_size] for i in range(0, len(jobs), batch_size)]
    n_workers = workers or os.cpu_count()

    click.echo(
        f"Found {len(files)} files. {len(batches)} batches of ≤{batch_size}, {n_workers} workers. Using {mscore_bin}",
        err=True,
    )

    ok = failed = 0
    log_fh = open(log_path, "a", encoding="utf-8") if log_path else None
    try:
        with tqdm(total=len(files), desc="gp2xml", file=sys.stderr) as pbar:
            with ThreadPoolExecutor(max_workers=n_workers) as executor:
                futures = {executor.submit(_run_json_batch, mscore_bin, batch, log_fh): batch for batch in batches}
                for fut in as_completed(futures):
                    b_ok, b_failed = fut.result()
                    ok += b_ok
                    failed += b_failed
                    pbar.update(len(futures[fut]))
    finally:
        if log_fh:
            log_fh.close()

    click.echo(f"\nDone. Converted: {ok}, Failed: {failed}", err=True)
    if failed and log_path:
        click.echo(f"Failures logged to: {log_path}", err=True)
