"""
SLTK Command Line Interface.

Usage:
    sltk extract <backend> <video> [-o output.h5]
    sltk segment <wilor.h5> [-o segments.json]
    sltk spot <video> -s segments.json -D dict/
    sltk nms <teaser.h5> [--nlf nlf.h5] [--video video.mp4]
    sltk pipeline <video> [-o output_dir/]
    sltk convert <input> <output> --from <format> --to <format>
    sltk evaluate <predictions> <references> --task <task>
    sltk to-elan <input> --output <output.eaf>
    sltk from-elan <input.eaf> --output <output.json>
"""

import json
from pathlib import Path

import click


@click.group()
@click.version_option(package_name="signlangtk")
def main():
    """Sign Language Toolkit - Tools for sign language research."""
    pass


@main.command()
@click.argument("input_path", type=click.Path(exists=True))
@click.argument("output_path", type=click.Path())
@click.option("--from", "from_format", required=True, help="Source pose format")
@click.option("--to", "to_format", required=True, help="Target pose format")
@click.option("--fps", type=float, default=25.0, help="Frames per second")
def convert(input_path, output_path, from_format, to_format, fps):
    """Convert poses between formats."""
    from sltk.data.core import PoseSequence

    try:
        click.echo(f"Loading {input_path} as {from_format}...")
        poses = PoseSequence.load(input_path, format=from_format, fps=fps)
        click.echo(f"  Loaded {poses.num_frames} frames")

        if from_format != to_format:
            click.echo(f"Converting to {to_format}...")
            poses = poses.to_format(to_format)

        click.echo(f"Saving to {output_path}...")
        poses.save(output_path)

        click.echo("Done!")
    except FileNotFoundError:
        raise click.ClickException(f"Input file not found: {input_path}")
    except ValueError as e:
        raise click.ClickException(f"Invalid pose format or data: {e}")
    except PermissionError as e:
        raise click.ClickException(f"Permission denied: {e}")
    except Exception as e:
        raise click.ClickException(f"Conversion failed: {e}")


@main.command()
@click.argument("predictions", type=click.Path(exists=True))
@click.argument("references", type=click.Path(exists=True))
@click.option(
    "--task",
    type=click.Choice(["translation", "segmentation"]),
    required=True,
    help="Evaluation task",
)
@click.option("--output", "-o", type=click.Path(), help="Output JSON file")
@click.option("--tolerance", type=float, default=0.1, help="Boundary tolerance (segmentation)")
@click.option("--encoding", default="utf-8", help="File encoding (default: utf-8)")
def evaluate(predictions, references, task, output, tolerance, encoding):
    """Evaluate predictions against references."""
    # Validate files exist
    pred_path = Path(predictions)
    ref_path = Path(references)

    if not pred_path.exists():
        raise click.ClickException(f"Predictions file not found: {predictions}")
    if not ref_path.exists():
        raise click.ClickException(f"References file not found: {references}")

    try:
        if task == "translation":
            from sltk.metrics.translation import translation_metrics

            try:
                with open(predictions, encoding=encoding) as f:
                    preds = [line.strip() for line in f if line.strip()]
            except UnicodeDecodeError as e:
                raise click.ClickException(
                    f"Failed to read predictions file with encoding '{encoding}': {e}"
                )

            try:
                with open(references, encoding=encoding) as f:
                    refs = [line.strip() for line in f if line.strip()]
            except UnicodeDecodeError as e:
                raise click.ClickException(
                    f"Failed to read references file with encoding '{encoding}': {e}"
                )

            if len(preds) != len(refs):
                raise click.ClickException(
                    f"Length mismatch: {len(preds)} predictions vs {len(refs)} references"
                )

            if not preds:
                raise click.ClickException("Predictions file is empty")
            if not refs:
                raise click.ClickException("References file is empty")

            results = translation_metrics(preds, refs)

        elif task == "segmentation":
            from sltk.io.elan import read_eaf
            from sltk.metrics.segmentation import segmentation_metrics

            try:
                preds = read_eaf(predictions)
            except Exception as e:
                raise click.ClickException(f"Failed to read predictions ELAN file: {e}")

            try:
                refs = read_eaf(references)
            except Exception as e:
                raise click.ClickException(f"Failed to read references ELAN file: {e}")

            results = segmentation_metrics(preds, refs, tolerance_sec=tolerance)

        # Display results
        click.echo("\nResults:")
        for metric, value in sorted(results.items()):
            click.echo(f"  {metric}: {value:.4f}")

        # Save if requested
        if output:
            try:
                with open(output, "w", encoding="utf-8") as f:
                    json.dump(results, f, indent=2)
                click.echo(f"\nSaved to {output}")
            except PermissionError:
                raise click.ClickException(f"Permission denied writing to: {output}")
            except Exception as e:
                raise click.ClickException(f"Failed to save output: {e}")

    except click.ClickException:
        raise
    except Exception as e:
        raise click.ClickException(f"Evaluation failed: {e}")


@main.command("to-elan")
@click.argument("input_path", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), required=True, help="Output .eaf file")
@click.option("--video", type=click.Path(), help="Source video to link")
@click.option("--tier", default="default", help="ELAN tier name")
def to_elan(input_path, output, video, tier):
    """Export segments to ELAN format.

    Input should be a JSON file with segments:
    [{"start": 0.0, "end": 1.5, "label": "HELLO"}, ...]
    """
    from sltk.data.core import Segment, SegmentList

    # Validate input file exists
    input_path_obj = Path(input_path)
    if not input_path_obj.exists():
        raise click.ClickException(f"Input file not found: {input_path}")

    # Parse JSON
    try:
        with open(input_path, encoding="utf-8") as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        raise click.ClickException(f"Invalid JSON in input file: {e}")
    except UnicodeDecodeError as e:
        raise click.ClickException(f"Failed to read input file (encoding error): {e}")
    except Exception as e:
        raise click.ClickException(f"Failed to read input file: {e}")

    # Validate JSON schema
    if not isinstance(data, list):
        raise click.ClickException("Invalid JSON schema: expected a list of segment objects")

    if not data:
        raise click.ClickException("Input file contains no segments")

    # Validate each segment has required fields
    for i, segment in enumerate(data):
        if not isinstance(segment, dict):
            raise click.ClickException(
                f"Invalid segment at index {i}: expected an object, got {type(segment).__name__}"
            )
        if "start" not in segment:
            raise click.ClickException(f"Segment at index {i} missing required field 'start'")
        if "end" not in segment:
            raise click.ClickException(f"Segment at index {i} missing required field 'end'")

        # Validate types
        try:
            float(segment["start"])
            float(segment["end"])
        except (TypeError, ValueError):
            raise click.ClickException(
                f"Segment at index {i} has invalid start/end values (must be numeric)"
            )

    try:
        segments = SegmentList(
            [
                Segment(
                    start=s["start"],
                    end=s["end"],
                    label=s.get("label"),
                    tier=s.get("tier", tier),
                    confidence=s.get("confidence"),
                )
                for s in data
            ]
        )

        segments.to_elan(output, video_path=video)
        click.echo(f"Exported {len(segments)} segments to {output}")
    except PermissionError:
        raise click.ClickException(f"Permission denied writing to: {output}")
    except Exception as e:
        raise click.ClickException(f"Failed to export to ELAN: {e}")


@main.command("from-elan")
@click.argument("eaf_path", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), help="Output JSON file")
@click.option("--tier", multiple=True, help="Tiers to extract (default: all)")
def from_elan(eaf_path, output, tier):
    """Import segments from ELAN format."""
    from sltk.io.elan import read_eaf

    # Validate input file
    eaf_path_obj = Path(eaf_path)
    if not eaf_path_obj.exists():
        raise click.ClickException(f"ELAN file not found: {eaf_path}")

    if eaf_path_obj.suffix.lower() != ".eaf":
        click.echo(f"Warning: File does not have .eaf extension: {eaf_path}")

    # Read ELAN file
    try:
        tiers = list(tier) if tier else None
        segments = read_eaf(eaf_path, tiers=tiers)
    except FileNotFoundError:
        raise click.ClickException(f"ELAN file not found: {eaf_path}")
    except PermissionError:
        raise click.ClickException(f"Permission denied reading: {eaf_path}")
    except Exception as e:
        # Handle corrupt or malformed ELAN files
        error_msg = str(e)
        if "xml" in error_msg.lower() or "parse" in error_msg.lower():
            raise click.ClickException(
                f"Failed to parse ELAN file (possibly corrupt or invalid XML): {e}"
            )
        raise click.ClickException(f"Failed to read ELAN file: {e}")

    click.echo(f"Loaded {len(segments)} segments from {len(segments.tiers)} tiers:")
    for tier_name in segments.tiers:
        tier_segs = segments.filter_by_tier(tier_name)
        click.echo(f"  {tier_name}: {len(tier_segs)} segments")

    if output:
        try:
            data = [
                {
                    "start": s.start,
                    "end": s.end,
                    "duration": s.duration,
                    "label": s.label,
                    "tier": s.tier,
                    "confidence": s.confidence,
                }
                for s in segments
            ]
            with open(output, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2)
            click.echo(f"\nSaved to {output}")
        except PermissionError:
            raise click.ClickException(f"Permission denied writing to: {output}")
        except Exception as e:
            raise click.ClickException(f"Failed to save output: {e}")


@main.command("info")
@click.argument("path", type=click.Path(exists=True))
def info(path):
    """Show information about a file (H5, ELAN, etc.)."""
    path = Path(path)

    if not path.exists():
        raise click.ClickException(f"File not found: {path}")

    suffix = path.suffix.lower()

    try:
        if suffix == ".h5":
            from sltk.io.h5 import get_h5_metadata

            try:
                meta = get_h5_metadata(path)
            except PermissionError:
                raise click.ClickException(f"Permission denied reading: {path}")
            except Exception as e:
                raise click.ClickException(f"Failed to read H5 file: {e}")

            click.echo(f"H5 File: {path.name}")
            click.echo(f"  Format: {meta.get('format', 'unknown')}")
            click.echo(f"  Frames: {meta.get('num_frames', 'unknown')}")
            click.echo(f"  Features: {', '.join(meta.get('features', [])[:5])}")
            if len(meta.get("features", [])) > 5:
                click.echo(f"    ... and {len(meta['features']) - 5} more")

        elif suffix == ".eaf":
            from sltk.io.elan import get_eaf_metadata, read_eaf

            try:
                meta = get_eaf_metadata(path)
                segments = read_eaf(path)
            except PermissionError:
                raise click.ClickException(f"Permission denied reading: {path}")
            except Exception as e:
                error_msg = str(e)
                if "xml" in error_msg.lower() or "parse" in error_msg.lower():
                    raise click.ClickException(
                        f"Failed to parse ELAN file (possibly corrupt or invalid XML): {e}"
                    )
                raise click.ClickException(f"Failed to read ELAN file: {e}")

            click.echo(f"ELAN File: {path.name}")
            click.echo(f"  Author: {meta.get('author', 'unknown')}")
            click.echo(f"  Tiers: {', '.join(meta.get('tiers', []))}")
            click.echo(f"  Total segments: {len(segments)}")
            for tier_name in meta.get("tiers", []):
                tier_segs = segments.filter_by_tier(tier_name)
                click.echo(f"    {tier_name}: {len(tier_segs)} segments")

        else:
            raise click.ClickException(
                f"Unsupported file type: {suffix}. Supported types: .h5, .eaf"
            )

    except click.ClickException:
        raise
    except Exception as e:
        raise click.ClickException(f"Failed to read file info: {e}")


@main.command()
@click.option("--host", default="0.0.0.0", help="Host to bind to")
@click.option("--port", "-p", default=8000, type=int, help="Port to serve on")
@click.option("--reload", is_flag=True, help="Enable auto-reload for development")
def serve(host, port, reload):
    """Start the SLTK web application (API + frontend)."""
    try:
        import uvicorn
    except ImportError:
        raise click.ClickException(
            "uvicorn is required to run the server. Install with: pip install sltk[api]"
        )

    static_dir = Path(__file__).resolve().parent.parent / "api" / "static"
    if static_dir.is_dir():
        click.echo(f"Serving web UI at http://localhost:{port}")
    else:
        click.echo(f"No frontend build found at {static_dir}")
        click.echo("API-only mode — web UI not available")

    click.echo(f"API docs at http://localhost:{port}/docs")

    uvicorn.run(
        "sltk.api.main:app",
        host=host,
        port=port,
        reload=reload,
        reload_dirs=["sltk"] if reload else None,
        log_level="info",
    )


@main.command("formats")
def formats():
    """List supported pose formats."""
    from sltk.data.formats import list_formats

    fmts = list_formats()
    click.echo("Supported pose formats:\n")

    for name, info in sorted(fmts.items()):
        status = []
        if info.get("can_load"):
            status.append("load")
        if info.get("can_save"):
            status.append("save")

        desc = info.get("description", "")
        click.echo(f"  {name}")
        if desc:
            click.echo(f"    {desc}")
        click.echo(f"    Capabilities: {', '.join(status)}")
        click.echo()


# =============================================================================
# Annotation Conversion Commands
# =============================================================================


@main.command("convert-annotations")
@click.argument("dataset", type=str)
@click.option(
    "--source",
    "-s",
    type=click.Path(exists=True),
    required=True,
    help="Source directory with raw annotations",
)
@click.option(
    "--output",
    "-o",
    type=click.Path(),
    required=True,
    help="Output directory for unified annotations",
)
@click.option(
    "--format",
    "-f",
    "source_format",
    type=str,
    default=None,
    help="Source format (auto-detected if not specified)",
)
@click.option(
    "--generate-manifest/--no-manifest",
    default=True,
    help="Generate manifest.json after conversion",
)
def convert_annotations(dataset, source, output, source_format, generate_manifest):
    """Convert raw annotations to unified format.

    Supported formats: elan, wlasl_json, msasl_json, asldict_json,
    how2sign_csv, asl_citizen_csv, bobsl, csl_daily_json, vtt, phoenix_csv, ytsl_tsv
    """
    from sltk.io.annotations import generate_manifest as gen_manifest
    from sltk.io.converters import convert_dataset

    # Validate source directory exists
    source_path = Path(source)
    if not source_path.exists():
        raise click.ClickException(f"Source directory not found: {source}")
    if not source_path.is_dir():
        raise click.ClickException(f"Source path is not a directory: {source}")

    # Create output directory if needed
    output_path = Path(output)
    try:
        output_path.mkdir(parents=True, exist_ok=True)
    except PermissionError:
        raise click.ClickException(f"Permission denied creating output directory: {output}")
    except Exception as e:
        raise click.ClickException(f"Failed to create output directory: {e}")

    click.echo(f"Converting {dataset} annotations...")
    click.echo(f"  Source: {source}")
    click.echo(f"  Output: {output}")

    if source_format:
        click.echo(f"  Format: {source_format}")
    else:
        click.echo("  Format: auto-detect")

    try:
        # Run conversion
        result = convert_dataset(
            dataset=dataset,
            source_dir=source,
            output_dir=output,
            source_format=source_format,
        )

        click.echo("\nConversion complete:")
        click.echo(f"  Files created: {result.files_created}")
        click.echo(f"  Duration: {result.duration_sec:.2f}s")

        if result.errors:
            click.echo(f"  Errors: {len(result.errors)}")
            for err in result.errors[:5]:
                click.echo(f"    - {err}")
            if len(result.errors) > 5:
                click.echo(f"    ... and {len(result.errors) - 5} more")

        # Generate manifest
        if generate_manifest and result.files_created > 0:
            click.echo("\nGenerating manifest...")
            try:
                manifest = gen_manifest(
                    annotation_dir=output,
                    dataset=dataset,
                    source_format=result.source_format,
                )
                stats = manifest.get("statistics", {})
                click.echo(f"  Total samples: {stats.get('total_samples', 0)}")
                click.echo(f"  Total segments: {stats.get('total_segments', 0)}")
                click.echo(f"  Unique glosses: {stats.get('unique_glosses', 0)}")
            except Exception as e:
                click.echo(f"  Warning: Failed to generate manifest: {e}")

    except ValueError as e:
        raise click.ClickException(f"Invalid configuration: {e}")
    except PermissionError as e:
        raise click.ClickException(f"Permission denied: {e}")
    except Exception as e:
        raise click.ClickException(f"Annotation conversion failed: {e}")


@main.command("validate-annotations")
@click.argument("path", type=click.Path(exists=True))
@click.option(
    "--strict/--no-strict",
    default=False,
    help="Use strict JSON schema validation (requires jsonschema)",
)
def validate_annotations(path, strict):
    """Validate unified annotation files.

    PATH can be a single JSON file or a directory of annotations.
    """
    from sltk.io.annotations import validate_annotation

    path = Path(path)

    # Validate path exists
    if not path.exists():
        raise click.ClickException(f"Path not found: {path}")

    try:
        files_to_check = []

        if path.is_file():
            if path.suffix.lower() != ".json":
                raise click.ClickException(f"Expected a JSON file, got: {path.suffix}")
            files_to_check = [path]
        else:
            if not path.is_dir():
                raise click.ClickException(f"Path is not a file or directory: {path}")
            files_to_check = list(path.glob("*.json"))
            # Exclude manifest
            files_to_check = [f for f in files_to_check if f.name != "manifest.json"]

        if not files_to_check:
            raise click.ClickException(f"No JSON files found in: {path}")

        click.echo(f"Validating {len(files_to_check)} files...")

        valid_count = 0
        invalid_count = 0
        all_errors = []

        for file_path in files_to_check:
            try:
                is_valid, errors = validate_annotation(file_path, strict=strict)

                if is_valid:
                    valid_count += 1
                else:
                    invalid_count += 1
                    all_errors.append((file_path.name, errors))
            except json.JSONDecodeError as e:
                invalid_count += 1
                all_errors.append((file_path.name, [f"Invalid JSON: {e}"]))
            except PermissionError:
                invalid_count += 1
                all_errors.append((file_path.name, ["Permission denied"]))
            except Exception as e:
                invalid_count += 1
                all_errors.append((file_path.name, [f"Error reading file: {e}"]))

        click.echo("\nResults:")
        click.echo(f"  Valid: {valid_count}")
        click.echo(f"  Invalid: {invalid_count}")

        if all_errors:
            click.echo("\nErrors:")
            for filename, errors in all_errors[:10]:
                click.echo(f"  {filename}:")
                for err in errors[:3]:
                    click.echo(f"    - {err}")
                if len(errors) > 3:
                    click.echo(f"    ... and {len(errors) - 3} more")

            if len(all_errors) > 10:
                click.echo(f"\n  ... and {len(all_errors) - 10} more files with errors")

    except click.ClickException:
        raise
    except Exception as e:
        raise click.ClickException(f"Validation failed: {e}")


@main.command("generate-manifest")
@click.argument("annotation_dir", type=click.Path(exists=True))
@click.option("--dataset", "-d", type=str, required=True, help="Dataset name")
@click.option(
    "--format", "-f", "source_format", type=str, default=None, help="Original annotation format"
)
def generate_manifest_cmd(annotation_dir, dataset, source_format):
    """Generate a manifest.json for a unified annotation directory."""
    from sltk.io.annotations import generate_manifest

    # Validate directory exists
    dir_path = Path(annotation_dir)
    if not dir_path.exists():
        raise click.ClickException(f"Directory not found: {annotation_dir}")
    if not dir_path.is_dir():
        raise click.ClickException(f"Path is not a directory: {annotation_dir}")

    # Check for JSON files
    json_files = list(dir_path.glob("*.json"))
    # Exclude existing manifest
    json_files = [f for f in json_files if f.name != "manifest.json"]
    if not json_files:
        raise click.ClickException(f"No annotation JSON files found in: {annotation_dir}")

    click.echo(f"Generating manifest for {dataset}...")
    click.echo(f"  Found {len(json_files)} annotation files")

    try:
        manifest = generate_manifest(
            annotation_dir=annotation_dir,
            dataset=dataset,
            source_format=source_format,
        )

        stats = manifest.get("statistics", {})
        click.echo("\nManifest generated:")
        click.echo(f"  Total samples: {stats.get('total_samples', 0)}")
        click.echo(f"  Total segments: {stats.get('total_segments', 0)}")
        click.echo(f"  Unique glosses: {stats.get('unique_glosses', 0)}")
        click.echo(f"  Unique signers: {stats.get('unique_signers', 0)}")
        click.echo(f"  Total duration: {stats.get('total_duration_sec', 0):.1f}s")

        if stats.get("samples_by_split"):
            click.echo("  Splits:")
            for split, count in stats["samples_by_split"].items():
                click.echo(f"    {split}: {count}")

        click.echo(f"\nSaved to {annotation_dir}/manifest.json")

    except PermissionError:
        raise click.ClickException(f"Permission denied writing manifest to: {annotation_dir}")
    except json.JSONDecodeError as e:
        raise click.ClickException(f"Invalid JSON in annotation files: {e}")
    except Exception as e:
        raise click.ClickException(f"Failed to generate manifest: {e}")


@main.command("annotation-formats")
def annotation_formats():
    """List supported annotation formats for conversion."""
    from sltk.io.converters import list_converters

    converters = list_converters()
    click.echo("Supported annotation formats:\n")

    format_info = {
        "elan": "ELAN .eaf files (BSLCP, BSLCP2)",
        "wlasl_json": "WLASL JSON format (nested gloss/instances)",
        "msasl_json": "MS-ASL JSON format (flat array)",
        "asldict_json": "ASLDict/BSLDict JSON (word entries)",
        "how2sign_csv": "How2Sign CSV (VIDEO_ID, SENTENCE, etc.)",
        "asl_citizen_csv": "ASL Citizen CSV format",
        "phoenix_csv": "Phoenix-2014 CSV (pipe-delimited)",
        "bobsl": "BOBSL TSV + VTT subtitles",
        "csl_daily_json": "CSL-Daily JSON format",
        "vtt": "WebVTT/SRT subtitle files",
        "bslzone": "BSLZone VTT + scene lists",
        "ytsl_tsv": "YTSL-25 TSV format",
        "youtube_asl_json": "YouTube-ASL JSON captions (start/duration/text)",
    }

    for name in sorted(converters):
        desc = format_info.get(name, "")
        click.echo(f"  {name}")
        if desc:
            click.echo(f"    {desc}")
        click.echo()


@main.command("annotation-info")
@click.argument("path", type=click.Path(exists=True))
def annotation_info(path):
    """Show information about a unified annotation file or directory."""
    from sltk.io.annotations import read_manifest, read_unified_annotation

    path = Path(path)

    # Validate path exists
    if not path.exists():
        raise click.ClickException(f"Path not found: {path}")

    try:
        if path.is_file():
            if path.name == "manifest.json":
                # Show manifest info
                try:
                    manifest = read_manifest(path)
                except json.JSONDecodeError as e:
                    raise click.ClickException(f"Invalid JSON in manifest: {e}")
                except PermissionError:
                    raise click.ClickException(f"Permission denied reading: {path}")

                click.echo(f"Manifest: {path.parent.name}")
                click.echo(f"  Dataset: {manifest.get('dataset')}")
                click.echo(f"  Version: {manifest.get('version')}")
                click.echo(f"  Source format: {manifest.get('source_format', 'unknown')}")

                stats = manifest.get("statistics", {})
                click.echo("\nStatistics:")
                click.echo(f"  Total samples: {stats.get('total_samples', 0)}")
                click.echo(f"  Total segments: {stats.get('total_segments', 0)}")
                click.echo(f"  Unique glosses: {stats.get('unique_glosses', 0)}")
                click.echo(f"  Unique signers: {stats.get('unique_signers', 0)}")

                if stats.get("samples_by_split"):
                    click.echo("\n  Samples by split:")
                    for split, count in stats["samples_by_split"].items():
                        click.echo(f"    {split}: {count}")
            else:
                # Show single annotation info
                if path.suffix.lower() != ".json":
                    raise click.ClickException(f"Expected a JSON file, got: {path.suffix}")

                try:
                    sample = read_unified_annotation(path)
                except json.JSONDecodeError as e:
                    raise click.ClickException(f"Invalid JSON in annotation file: {e}")
                except PermissionError:
                    raise click.ClickException(f"Permission denied reading: {path}")
                except KeyError as e:
                    raise click.ClickException(f"Missing required field in annotation: {e}")

                click.echo(f"Sample: {sample.id}")
                click.echo(f"  Dataset: {sample.dataset}")
                click.echo(f"  Split: {sample.split or 'unknown'}")

                if sample.segments:
                    click.echo(f"  Segments: {len(sample.segments)}")
                    for tier in sample.segments.tiers:
                        tier_segs = sample.segments.filter_by_tier(tier)
                        click.echo(f"    {tier}: {len(tier_segs)}")

                if sample.glosses:
                    click.echo(f"  Glosses: {len(sample.glosses)}")
                    if len(sample.glosses) <= 10:
                        click.echo(f"    {' '.join(sample.glosses)}")

                if sample.text:
                    text_preview = (
                        sample.text[:100] + "..." if len(sample.text) > 100 else sample.text
                    )
                    click.echo(f"  Text: {text_preview}")

        else:
            # Directory - show summary
            if not path.is_dir():
                raise click.ClickException(f"Path is not a file or directory: {path}")

            manifest_path = path / "manifest.json"
            if manifest_path.exists():
                try:
                    manifest = read_manifest(manifest_path)
                except json.JSONDecodeError as e:
                    raise click.ClickException(f"Invalid JSON in manifest: {e}")
                except PermissionError:
                    raise click.ClickException(
                        f"Permission denied reading manifest: {manifest_path}"
                    )

                stats = manifest.get("statistics", {})
                click.echo(f"Dataset: {manifest.get('dataset')}")
                click.echo(f"  Samples: {stats.get('total_samples', 0)}")
                click.echo(f"  Segments: {stats.get('total_segments', 0)}")
                click.echo(f"  Glosses: {stats.get('unique_glosses', 0)}")
            else:
                # Count files
                json_files = list(path.glob("*.json"))
                click.echo(f"Directory: {path.name}")
                click.echo(f"  JSON files: {len(json_files)}")
                click.echo("  (No manifest.json found - run 'sltk generate-manifest')")

    except click.ClickException:
        raise
    except Exception as e:
        raise click.ClickException(f"Failed to read annotation info: {e}")


# =============================================================================
# Extraction Commands
# =============================================================================

EXTRACTOR_BACKENDS = ["wilor", "nlf", "teaser"]


@main.command("extract")
@click.argument("backend", type=click.Choice(EXTRACTOR_BACKENDS))
@click.argument("video", type=click.Path(exists=True))
@click.option(
    "--output",
    "-o",
    type=click.Path(),
    default=None,
    help="Output H5 path (default: <stem>_<backend>.h5)",
)
@click.option("--device", "-d", default="cuda:0", help="Torch device (default: cuda:0)")
@click.option("--batch-size", "-b", type=int, default=None, help="Frames per GPU batch")
@click.option(
    "--compile", is_flag=True, default=False, help="Enable torch.compile (slow first run)"
)
def extract(backend, video, output, device, batch_size, compile):
    """Extract poses from video.

    \b
    Backends:
      wilor   - 3D hand keypoints + MANO rotations (~2.5 GB weights)
      nlf     - 55 SMPL-X body joints + params (~540 MB weights)
      teaser  - FLAME face parameters (~360 MB weights)

    \b
    Examples:
      sltk extract wilor video.mp4
      sltk extract wilor video.mp4 -o hands.h5
      sltk extract teaser video.mp4 --device cpu
    """
    import torch

    video_path = Path(video)
    if output is None:
        output = str(video_path.with_name(f"{video_path.stem}_{backend}.h5"))

    click.echo(f"Extracting {backend} from {video_path.name}...")

    try:
        if backend == "wilor":
            from sltk.extraction.wilor import WiLoRConfig, WiLoRExtractor

            config = WiLoRConfig(device=device, use_compile=compile)
            if batch_size is not None:
                config.img_batch_size = batch_size
            with WiLoRExtractor(config) as ext:
                ext.load_model()
                result = ext.extract_from_video(video, output)

        elif backend == "nlf":
            from sltk.extraction.nlf import NLFConfig, NLFExtractor

            config = NLFConfig(device=device)
            if batch_size is not None:
                config.img_batch_size = batch_size
            with NLFExtractor(config) as ext:
                ext.load_model()
                result = ext.extract_from_video(video, output)

        elif backend == "teaser":
            from sltk.extraction.teaser import TeaserConfig, TeaserExtractor

            config = TeaserConfig(device=device)
            if batch_size is not None:
                config.img_batch_size = batch_size
            with TeaserExtractor(config) as ext:
                ext.load_model()
                result = ext.extract_from_video(video, output)

        torch.cuda.empty_cache()

        click.echo(f"  Frames:     {result.num_frames}")
        click.echo(f"  Detections: {result.num_detections}")
        click.echo(f"  FPS:        {result.fps}")
        click.echo(f"  Output:     {output}")

    except FileNotFoundError as e:
        raise click.ClickException(str(e))
    except RuntimeError as e:
        if "CUDA" in str(e) or "out of memory" in str(e):
            raise click.ClickException(
                f"GPU error: {e}\nTry reducing --batch-size or using --device cpu"
            )
        raise click.ClickException(f"Extraction failed: {e}")
    except Exception as e:
        raise click.ClickException(f"Extraction failed: {e}")


# =============================================================================
# Segmentation Command
# =============================================================================


_VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".mpg", ".mpeg"}


@main.command("segment")
@click.argument("input_path", type=click.Path(exists=True))
@click.option(
    "--output", "-o", type=click.Path(), default=None, help="Output path (.json, .eaf, or .pt)"
)
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["json", "elan", "pt"]),
    default="json",
    help="Output format",
)
@click.option(
    "--fps", type=float, default=None, help="Video FPS (auto-detected from video/H5 if omitted)"
)
@click.option("--video", type=click.Path(), default=None, help="Video path to link in ELAN output")
@click.option("--device", "-d", default="auto", help="Torch device")
@click.option(
    "--batch-size",
    "-b",
    type=int,
    default=None,
    help="WiLoR batch size (only when input is a video)",
)
@click.option(
    "--keep-h5/--no-keep-h5", default=True, help="Keep intermediate WiLoR H5 when input is a video"
)
def segment(input_path, output, output_format, fps, video, device, batch_size, keep_h5):
    """Segment signs from WiLoR H5 features or directly from video.

    INPUT_PATH can be a .h5 file, a directory of .h5 files, or a video file.
    When a video is given, WiLoR hand extraction runs automatically first.

    \b
    Examples:
      sltk segment video_wilor.h5
      sltk segment video.mp4                          # auto-extracts WiLoR first
      sltk segment video.mp4 -f elan -o segments.eaf
      sltk segment video_wilor.h5 -o segments.eaf -f elan --video video.mp4
      sltk segment poses/ -o results.json
    """
    from sltk.segmentation.output import OutputFormat
    from sltk.segmentation.postprocess import extract_segments
    from sltk.segmentation.runner import segment_h5

    input_p = Path(input_path)
    is_video = input_p.is_file() and input_p.suffix.lower() in _VIDEO_EXTENSIONS
    h5_input = input_path
    extracted_fps = None

    # ── If input is a video, run WiLoR extraction first ────────
    if is_video:
        import torch

        wilor_h5 = str(input_p.with_name(f"{input_p.stem}_wilor.h5"))

        click.echo(f"[1/2] Extracting WiLoR hands from {input_p.name}...")
        click.echo("  This requires a CUDA GPU and ~2.5 GB of model weights.")

        try:
            from sltk.extraction.wilor import WiLoRConfig, WiLoRExtractor

            wilor_device = "cuda:0" if device == "auto" else device
            config = WiLoRConfig(device=wilor_device)
            if batch_size is not None:
                config.img_batch_size = batch_size

            with WiLoRExtractor(config) as ext:
                click.echo("  Loading WiLoR model...")
                ext.load_model()
                click.echo("  Running hand detection + pose estimation...")
                wilor_result = ext.extract_from_video(str(input_p), wilor_h5)

            extracted_fps = wilor_result.fps
            torch.cuda.empty_cache()

            click.echo(
                f"  Done: {wilor_result.num_frames} frames, "
                f"{wilor_result.num_detections} hand detections, "
                f"{extracted_fps:.1f} FPS"
            )
            click.echo(f"  WiLoR H5: {wilor_h5}")

        except RuntimeError as e:
            if "CUDA" in str(e) or "out of memory" in str(e):
                raise click.ClickException(
                    f"GPU error during WiLoR extraction: {e}\n"
                    "Try reducing --batch-size or pre-extracting with: "
                    "sltk extract wilor video.mp4 --device cpu"
                )
            raise click.ClickException(f"WiLoR extraction failed: {e}")
        except Exception as e:
            raise click.ClickException(f"WiLoR extraction failed: {e}")

        h5_input = wilor_h5
        # Default video link to the input video for ELAN output
        if video is None:
            video = str(input_p)

        step_label = "[2/2] "
    else:
        step_label = ""

    # ── Resolve FPS ────────────────────────────────────────────
    if fps is None:
        fps = extracted_fps or 25.0

    # ── Resolve output path ────────────────────────────────────
    fmt_map = {"json": OutputFormat.JSON, "elan": OutputFormat.ELAN, "pt": OutputFormat.PT}
    fmt = fmt_map[output_format]

    if output is None:
        suffix = {"json": ".json", "elan": ".eaf", "pt": ".pt"}[output_format]
        stem = input_p.stem if is_video else Path(h5_input).stem
        output = str(input_p.with_name(f"{stem}_segments{suffix}"))

    # ── Run segmentation ───────────────────────────────────────
    click.echo(f"{step_label}Segmenting{' ' + Path(h5_input).name if not is_video else ''}...")
    click.echo("  Loading segmentation model (~180 MB)...")

    try:
        predictions = segment_h5(
            h5_input,
            output_path=output,
            output_format=fmt,
            fps=fps,
            media_path=video,
            checkpoint_path=None,
        )

        for name, labels in predictions.items():
            segs = extract_segments(labels)
            n_frames = len(labels)
            click.echo(f"  {name}: {len(segs)} segments ({n_frames} frames)")

        click.echo(f"  Output: {output}")

        # Clean up intermediate H5 if requested
        if is_video and not keep_h5:
            Path(wilor_h5).unlink(missing_ok=True)
            click.echo(f"  Removed intermediate: {wilor_h5}")

    except FileNotFoundError as e:
        raise click.ClickException(str(e))
    except Exception as e:
        raise click.ClickException(f"Segmentation failed: {e}")


# =============================================================================
# Gloss Spotting Command
# =============================================================================


@main.command("spot")
@click.argument("video", type=click.Path(exists=True))
@click.option(
    "--segments", "-s", type=click.Path(exists=True), required=True, help="Segments JSON file"
)
@click.option(
    "--dictionary",
    "-D",
    type=click.Path(exists=True),
    multiple=True,
    required=True,
    help="Dictionary directory (repeatable)",
)
@click.option("--output", "-o", type=click.Path(), default=None, help="Output .eaf path")
@click.option("--top-k", "-k", type=int, default=10, help="Top-K glosses per segment")
@click.option("--stride", type=int, default=4, help="Frame stride for feature extraction")
@click.option(
    "--pooling", type=click.Choice(["max", "mean"]), default="max", help="Segment pooling strategy"
)
def spot(video, segments, dictionary, output, top_k, stride, pooling):
    """Spot glosses in video segments using SignRep.

    Matches each detected segment against a dictionary of known signs
    by cosine similarity of 768-dim visual embeddings.

    \b
    Examples:
      sltk spot video.mp4 -s segments.json -D dict/ -o spotted.eaf
      sltk spot video.mp4 -s segments.json -D dict1/ -D dict2/ -k 5
    """
    from sltk.embedding.pipeline import SignRepPipeline

    video_path = Path(video)
    if output is None:
        output = str(video_path.with_name(f"{video_path.stem}_spotted.eaf"))

    click.echo(f"Spotting glosses in {video_path.name}...")

    try:
        pipeline = SignRepPipeline()
        result = pipeline.spot_from_video(
            video_path=str(video),
            segments_json=segments,
            dictionary_dirs=[str(d) for d in dictionary],
            top_k=top_k,
            stride=stride,
            segment_pooling=pooling,
        )

        click.echo(f"  Segments matched: {len(result.segments)}")

        # Show top matches for first few segments
        for seg in result.segments[:5]:
            if seg.top_glosses:
                best = seg.top_glosses[0]
                click.echo(
                    f"    {seg.start_ms}ms–{seg.end_ms}ms: "
                    f"{best['gloss']} ({best['similarity']:.3f})"
                )
        if len(result.segments) > 5:
            click.echo(f"    ... and {len(result.segments) - 5} more")

        result.save_eaf(output, media_path=str(video))
        click.echo(f"  Output: {output}")

    except FileNotFoundError as e:
        raise click.ClickException(str(e))
    except Exception as e:
        raise click.ClickException(f"Gloss spotting failed: {e}")


# =============================================================================
# NMS Detection Command
# =============================================================================


@main.command("nms")
@click.argument("teaser_h5", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), default=None, help="Output directory")
@click.option(
    "--format",
    "-f",
    "formats",
    type=click.Choice(["eaf", "json", "csv"]),
    multiple=True,
    default=["eaf"],
    help="Output formats (repeatable)",
)
@click.option("--video", type=click.Path(), default=None, help="Video path to link in ELAN output")
@click.option(
    "--nlf", type=click.Path(exists=True), default=None, help="NLF H5 file for gaze detection"
)
@click.option(
    "--detectors", type=str, default="all", help="Comma-separated detector names or 'all'"
)
@click.option("--no-adaptive", is_flag=True, default=False, help="Disable adaptive thresholds")
def nms(teaser_h5, output, formats, video, nlf, detectors, no_adaptive):
    """Detect non-manual signals from TEASER face data.

    Detects blinks, head nods/shakes/tilts, mouth movements,
    eyebrow raises, eye gaze, and squints.

    \b
    Examples:
      sltk nms video_teaser.h5
      sltk nms video_teaser.h5 --nlf video_nlf.h5 --video video.mp4
      sltk nms video_teaser.h5 --detectors blink,nod,shake -f json
    """
    from sltk.nms.runner import detect_nms, export_results

    h5_path = Path(teaser_h5)
    out_dir = output if output else str(h5_path.parent)

    det_set = set(d.strip() for d in detectors.split(","))

    click.echo(f"Detecting NMS in {h5_path.name}...")

    try:
        blinks, nms_events, quality = detect_nms(
            str(teaser_h5),
            detectors=det_set,
            no_adaptive=no_adaptive,
            smpl_path=nlf,
        )

        click.echo(f"  Tracking quality: {quality.detection_rate:.1%} detection rate")
        click.echo(f"  Blinks: {len(blinks)}")
        click.echo(f"  NMS events: {len(nms_events)}")

        # Count by tier
        from collections import Counter

        tier_counts = Counter(e.tier for e in nms_events)
        for tier, count in tier_counts.most_common():
            click.echo(f"    {tier:20s} {count:4d}")

        # Map format names
        fmt_map = {"eaf": "elan", "json": "json", "csv": "csv"}
        export_fmts = [fmt_map[f] for f in formats]

        paths = export_results(
            blinks,
            nms_events,
            str(teaser_h5),
            output_dir=out_dir,
            formats=export_fmts,
            media_path=video or "",
        )

        for fmt, path in paths.items():
            click.echo(f"  {fmt}: {path}")

    except FileNotFoundError as e:
        raise click.ClickException(str(e))
    except Exception as e:
        raise click.ClickException(f"NMS detection failed: {e}")


# =============================================================================
# Full Pipeline Command
# =============================================================================


@main.command("pipeline")
@click.argument("video", type=click.Path(exists=True))
@click.option(
    "--output", "-o", type=click.Path(), default=None, help="Output directory (default: ./output)"
)
@click.option("--device", "-d", default="cuda:0", help="Torch device")
@click.option("--skip-nlf", is_flag=True, default=False, help="Skip NLF body extraction")
@click.option(
    "--skip-teaser", is_flag=True, default=False, help="Skip TEASER face extraction and NMS"
)
@click.option(
    "--skip-nms", is_flag=True, default=False, help="Skip NMS detection (still extracts TEASER)"
)
@click.option(
    "--batch-size", "-b", type=int, default=None, help="Override batch size for all extractors"
)
def pipeline(video, output, device, skip_nlf, skip_teaser, skip_nms, batch_size):
    """Run the full extraction-to-ELAN pipeline on a video.

    Runs all extractors (WiLoR, NLF, TEASER), sign segmentation,
    NMS detection, and assembles a multi-tier ELAN file.

    \b
    Examples:
      sltk pipeline video.mp4
      sltk pipeline video.mp4 -o results/
      sltk pipeline video.mp4 --skip-nlf --device cuda:1
      sltk pipeline video.mp4 -b 32  # smaller batches for low VRAM
    """
    import torch

    video_path = Path(video)
    out_dir = Path(output) if output else Path("output")
    out_dir.mkdir(parents=True, exist_ok=True)
    stem = video_path.stem

    click.echo(f"Pipeline: {video_path.name}")
    click.echo(f"Output:   {out_dir}/")

    # ── Step 1: WiLoR extraction ───────────────────────────────
    wilor_h5 = str(out_dir / f"{stem}_wilor.h5")
    click.echo("\n[1/5] Extracting WiLoR hands...")
    try:
        from sltk.extraction.wilor import WiLoRConfig, WiLoRExtractor

        config = WiLoRConfig(device=device)
        if batch_size is not None:
            config.img_batch_size = batch_size
        with WiLoRExtractor(config) as ext:
            ext.load_model()
            wilor_result = ext.extract_from_video(video, wilor_h5)
        fps = wilor_result.fps
        torch.cuda.empty_cache()
        click.echo(f"  {wilor_result.num_frames} frames, {wilor_result.num_detections} detections")
    except Exception as e:
        raise click.ClickException(f"WiLoR extraction failed: {e}")

    # ── Step 2: NLF extraction (optional) ──────────────────────
    nlf_h5 = str(out_dir / f"{stem}_nlf.h5")
    if not skip_nlf:
        click.echo("\n[2/5] Extracting NLF body...")
        try:
            from sltk.extraction.nlf import NLFConfig, NLFExtractor

            config = NLFConfig(device=device)
            if batch_size is not None:
                config.img_batch_size = batch_size
            with NLFExtractor(config) as ext:
                ext.load_model()
                nlf_result = ext.extract_from_video(video, nlf_h5)
            torch.cuda.empty_cache()
            click.echo(f"  {nlf_result.num_frames} frames, {nlf_result.num_detections} detections")
        except Exception as e:
            raise click.ClickException(f"NLF extraction failed: {e}")
    else:
        nlf_h5 = None
        click.echo("\n[2/5] Skipping NLF")

    # ── Step 3: TEASER extraction (optional) ───────────────────
    teaser_h5 = str(out_dir / f"{stem}_teaser.h5")
    if not skip_teaser:
        click.echo("\n[3/5] Extracting TEASER face...")
        try:
            from sltk.extraction.teaser import TeaserConfig, TeaserExtractor

            config = TeaserConfig(device=device)
            if batch_size is not None:
                config.img_batch_size = batch_size
            with TeaserExtractor(config) as ext:
                ext.load_model()
                teaser_result = ext.extract_from_video(video, teaser_h5)
            torch.cuda.empty_cache()
            click.echo(
                f"  {teaser_result.num_frames} frames, {teaser_result.num_detections} detections"
            )
        except Exception as e:
            raise click.ClickException(f"TEASER extraction failed: {e}")
    else:
        teaser_h5 = None
        click.echo("\n[3/5] Skipping TEASER")

    # ── Step 4: Segmentation ───────────────────────────────────
    click.echo("\n[4/5] Segmenting signs...")
    try:
        from sltk.segmentation.h5_loader import h5_to_features
        from sltk.segmentation.postprocess import extract_segments, label_fixes
        from sltk.segmentation.runner import get_runner

        features = h5_to_features(wilor_h5)
        runner = get_runner()
        labels = label_fixes(runner.predict(features))
        segments = extract_segments(labels)
        click.echo(f"  {len(segments)} segments")
    except Exception as e:
        raise click.ClickException(f"Segmentation failed: {e}")

    # ── Step 5: NMS detection (optional) ───────────────────────
    blinks = []
    nms_events = []
    if teaser_h5 and not skip_nms:
        click.echo("\n[5/5] Detecting NMS...")
        try:
            from sltk.nms.runner import detect_nms

            blinks, nms_events, quality = detect_nms(
                teaser_h5,
                detectors={"all"},
                smpl_path=nlf_h5,
            )
            click.echo(f"  {len(blinks)} blinks, {len(nms_events)} NMS events")
            click.echo(f"  Tracking quality: {quality.detection_rate:.1%}")
        except Exception as e:
            click.echo(f"  Warning: NMS detection failed: {e}")
    else:
        click.echo("\n[5/5] Skipping NMS")

    # ── Build ELAN file ────────────────────────────────────────
    click.echo("\nBuilding ELAN file...")
    try:
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new(video_path=video)

        # Segmentation tier
        doc.add_tier("Segmentation")
        for start_frame, end_frame in segments:
            doc.add_segment("Segmentation", start_frame / fps, end_frame / fps, "SIGN")

        # NMS tiers
        if blinks or nms_events:
            nms_tier_names = [
                "BLINK",
                "HEAD-NOD",
                "HEAD-SHAKE",
                "HEAD-TILT",
                "MOUTH-MOVEMENT",
                "EYEBROW-RAISE",
                "EYEBROW-FURROW",
                "EYE-GAZE",
                "EYE-SQUINT",
            ]
            for tier_name in nms_tier_names:
                doc.add_tier(tier_name)

            for b in blinks:
                doc.add_segment("BLINK", b.start_frame / fps, b.end_frame / fps, "blink")

            for ev in nms_events:
                doc.add_segment(ev.tier, ev.start_frame / fps, ev.end_frame / fps, ev.label)

        eaf_path = str(out_dir / f"{stem}.eaf")
        doc.save(eaf_path)

        # Summary
        tiers = doc.get_tiers()
        total_annotations = sum(t.segment_count for t in tiers)
        click.echo(f"\nSaved: {eaf_path}")
        click.echo(f"  {len(tiers)} tiers, {total_annotations} annotations")
        for t in tiers:
            if t.segment_count > 0:
                click.echo(f"    {t.name:25s} {t.segment_count:4d}")

    except Exception as e:
        raise click.ClickException(f"ELAN file generation failed: {e}")


if __name__ == "__main__":
    main()
