"""
Command-Line Interface (CLI) for Verisoo.

This module provides the `verisoo` command-line tool for interacting with
the image provenance system. It wraps the core library functionality in
a user-friendly interface.

Command Structure:
-----------------

    verisoo
    ├── identity              # Manage attestor identity (Ed25519 keypair)
    │   ├── generate          # Create new identity
    │   └── show              # Display current identity fingerprint
    │
    ├── attest <image>        # Create attestation for an image
    │   ├── --location, -l    # GPS coordinates
    │   ├── --caption, -c     # Photographer's notes
    │   ├── --tag, -t         # Metadata tags (repeatable)
    │   └── --no-exif         # Disable EXIF extraction
    │
    ├── verify <image>        # Check image against known attestations
    │   └── --exact           # Require byte-exact match (no perceptual)
    │
    ├── log                   # Query the attestation log
    │   ├── status            # Show log statistics
    │   └── list              # List recent attestations
    │
    ├── peer                  # Manage federation peers
    │   ├── add <url> <fp>    # Add a peer node
    │   └── list              # List configured peers
    │
    └── serve                 # Run the API server
        ├── --host            # Bind address (default: 0.0.0.0)
        └── --port            # Listen port (default: 8000)

Global Options:
--------------
    --data-dir PATH     Override default data directory (~/.verisoo)
    --json              Output in JSON format (for scripting)

Data Directory Structure:
------------------------
    ~/.verisoo/
    ├── private.pem         # Ed25519 private key (PEM format)
    ├── public.pem          # Ed25519 public key (PEM format)
    ├── identity.json       # Identity metadata (name, created_at)
    ├── attestations.binlog # Append-only binary log of attestation records
    ├── index.lmdb/         # LMDB index for fast lookups
    ├── merkle.json         # Current Merkle tree root hash
    └── peers.json          # Federation peer list

Usage Examples:
--------------
    # First-time setup
    $ verisoo identity generate --name "Photographer Name"

    # Attest a photo with location
    $ verisoo attest photo.jpg -l "50.45,30.52,10,Kyiv" -c "Morning scene"

    # Verify an image (even after social media compression)
    $ verisoo verify downloaded_photo.jpg

    # Start API server for remote verification
    $ verisoo serve --port 8000

    # Check log status
    $ verisoo log status

Exit Codes:
----------
    0: Success
    1: Verification failed (no matching attestation found)
    2: Error (missing identity, invalid input, etc.)
"""

from __future__ import annotations

import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

# -----------------------------------------------------------------------------
# DEPENDENCY CHECK
# Click is required for CLI but optional for library-only usage
# -----------------------------------------------------------------------------
try:
    import click
except ImportError:
    print("CLI requires click: pip install verisoo[cli]", file=sys.stderr)
    sys.exit(1)


@click.group()
@click.option("--data-dir", type=click.Path(path_type=Path), help="Data directory")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
@click.pass_context
def main(ctx: click.Context, data_dir: Path | None, json_output: bool) -> None:
    """
    Verisoo - Decentralized image provenance and attestation.

    Part of the Soo Suite. Prove when images were created and by whom.
    """
    ctx.ensure_object(dict)
    ctx.obj["data_dir"] = data_dir
    ctx.obj["json"] = json_output


# --- Identity commands ---


@main.group()
def identity() -> None:
    """Manage attestor identity."""
    pass


@identity.command("generate")
@click.option("--name", help="Display name for this identity")
@click.option("--password", is_flag=True, help="Encrypt private key with password")
@click.pass_context
def identity_generate(ctx: click.Context, name: str | None, password: bool) -> None:
    """Generate a new attestor identity (Ed25519 keypair)."""
    from .crypto import generate_keypair, create_identity, save_private_key, save_public_key
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))

    if storage.has_node_identity():
        raise click.ClickException("Identity already exists. Use --force to overwrite.")

    # Generate keypair
    private_key, public_key = generate_keypair()

    # Get password if requested
    key_password = None
    if password:
        key_password = click.prompt("Password", hide_input=True, confirmation_prompt=True).encode()

    # Save keys
    save_private_key(private_key, storage.private_key_path, key_password)
    save_public_key(public_key, storage.public_key_path)

    # Create and save identity
    metadata = {"name": name} if name else {}
    ident = create_identity(private_key, metadata)
    storage.save_identity(ident)

    if ctx.obj.get("json"):
        click.echo(json.dumps({"fingerprint": ident.fingerprint, "created": ident.created_at.isoformat()}))
    else:
        click.echo(f"Identity created: {ident.fingerprint}")
        click.echo(f"Public key: {storage.public_key_path}")


@identity.command("show")
@click.pass_context
def identity_show(ctx: click.Context) -> None:
    """Show current identity."""
    from .crypto import load_public_key, fingerprint_from_pubkey
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))

    if not storage.has_node_identity():
        raise click.ClickException("No identity configured. Run: verisoo identity generate")

    public_key = load_public_key(storage.public_key_path)
    fingerprint = fingerprint_from_pubkey(public_key.public_key_bytes())

    if ctx.obj.get("json"):
        click.echo(json.dumps({"fingerprint": fingerprint}))
    else:
        click.echo(f"Fingerprint: {fingerprint}")
        click.echo(f"Public key:  {storage.public_key_path}")


# =============================================================================
# ATTESTATION COMMANDS
# =============================================================================
#
# The `attest` command is the primary user-facing operation. It:
#   1. Reads an image file
#   2. Optionally extracts EXIF metadata (enabled by default)
#   3. Merges user-provided metadata (location, caption, tags)
#   4. Creates a cryptographically signed attestation
#   5. Appends the record to the local log
#   6. Updates the Merkle tree root
#
# The result is a tamper-evident proof that the image existed at attestation
# time, attested by the configured identity.
# =============================================================================


def _parse_location(location_str: str) -> dict[str, Any]:
    """
    Parse a location string from CLI into a GeoLocation-compatible dict.

    This function provides a flexible, human-friendly way to specify GPS
    coordinates on the command line. It supports multiple formats to balance
    convenience with precision.

    Supported Formats:
    -----------------
    1. Basic coordinates:
       "50.4501,30.5234"
       → {"lat": 50.4501, "lon": 30.5234}

    2. With accuracy (meters):
       "50.4501,30.5234,10"
       → {"lat": 50.4501, "lon": 30.5234, "accuracy": 10.0}

    3. With accuracy and name:
       "50.4501,30.5234,10,Kyiv, Ukraine"
       → {"lat": 50.4501, "lon": 30.5234, "accuracy": 10.0, "name": "Kyiv, Ukraine"}

    4. With name only (if 3rd part is not a number):
       "50.4501,30.5234,Kyiv, Ukraine"
       → {"lat": 50.4501, "lon": 30.5234, "name": "Kyiv, Ukraine"}

    Note: The name can contain commas (e.g., "City, Country") because we
    use split(",", 3) to limit splitting to first 3 commas.

    Args:
        location_str: Comma-separated location string from CLI

    Returns:
        Dict with lat, lon, and optionally accuracy and name

    Raises:
        click.ClickException: If format is invalid or coordinates out of range

    Validation:
        - Latitude must be between -90 and 90 (poles)
        - Longitude must be between -180 and 180 (date line)
        - At minimum, lat and lon are required
    """
    # Split into at most 4 parts: lat, lon, accuracy, name
    # Using maxsplit=3 allows the name to contain commas
    parts = location_str.split(",", 3)

    # Validate minimum required parts
    if len(parts) < 2:
        raise click.ClickException(
            "Invalid location format. Use: lat,lon or lat,lon,accuracy or lat,lon,accuracy,name"
        )

    # Parse latitude and longitude
    try:
        lat = float(parts[0].strip())
        lon = float(parts[1].strip())
    except ValueError:
        raise click.ClickException("Invalid latitude/longitude values")

    # Validate coordinate ranges
    if not (-90 <= lat <= 90):
        raise click.ClickException("Latitude must be between -90 and 90")
    if not (-180 <= lon <= 180):
        raise click.ClickException("Longitude must be between -180 and 180")

    # Build location dict with required fields
    location: dict[str, Any] = {"lat": lat, "lon": lon}

    # Parse optional accuracy (3rd part)
    if len(parts) >= 3:
        try:
            accuracy = float(parts[2].strip())
            location["accuracy"] = accuracy
        except ValueError:
            # 3rd part is not a number - treat everything from here as name
            # This handles: "50.45,30.52,Kyiv, Ukraine" (no accuracy)
            location["name"] = ",".join(parts[2:]).strip()
            return location

    # Parse optional name (4th part, may contain commas)
    if len(parts) >= 4:
        location["name"] = parts[3].strip()

    return location


@main.command()
@click.argument("image", type=click.Path(exists=True, path_type=Path))
@click.option("--password", is_flag=True, help="Private key is encrypted")
@click.option("--tag", "-t", multiple=True, help="Add metadata tags")
@click.option("--location", "-l", "location_str", help='GPS coords: "lat,lon" or "lat,lon,accuracy,name"')
@click.option("--caption", "-c", help="Photographer's notes")
@click.option("--no-exif", "no_exif", is_flag=True, help="Disable auto EXIF extraction")
@click.option("--embed", "-e", is_flag=True, help="Embed proof link in image (JPEG: DCT, other: XMP sidecar)")
@click.option("--base-url", default="https://verisoo.io", help="Base URL for proof links")
@click.pass_context
def attest(
    ctx: click.Context,
    image: Path,
    password: bool,
    tag: tuple[str, ...],
    location_str: str | None,
    caption: str | None,
    no_exif: bool,
    embed: bool,
    base_url: str,
) -> None:
    """
    Create a cryptographic attestation for an image.

    This command creates a signed record proving that YOU attested THIS IMAGE
    at THIS TIME with THIS METADATA. The attestation is stored in your local
    log and can be synced to federation peers.

    \b
    METADATA SOURCES (in order of precedence):
    1. Command-line options (--location, --caption, --tag)
    2. EXIF data from the image (unless --no-exif)

    \b
    PROOF EMBEDDING (--embed):
    For JPEG: Uses DCT steganography (invisible, survives compression)
    For PNG/RAW: Creates XMP sidecar file readable by Lightroom/Darktable

    \b
    EXAMPLES:
        # Basic attestation (auto-extracts EXIF)
        verisoo attest photo.jpg

        # With proof link embedded in image
        verisoo attest photo.jpg --embed

        # With manual location (overrides EXIF GPS)
        verisoo attest photo.jpg -l "50.45,30.52,10,Kyiv"

        # With caption and tags
        verisoo attest photo.jpg -c "Morning scene" -t news -t ukraine

        # Skip EXIF extraction
        verisoo attest photo.jpg --no-exif

    \b
    OUTPUT:
        Attested: photo.jpg
          SHA-256: a1b2c3d4e5f6...
          Index:   42
          Root:    9f8e7d6c5b4a...
          Embedded: photo_attested.jpg (DCT)
    """
    from .attestation import create_attestation
    from .crypto import load_private_key
    from .storage import LocalStorage

    # -------------------------------------------------------------------------
    # Initialize storage and verify identity exists
    # -------------------------------------------------------------------------
    storage = LocalStorage(ctx.obj.get("data_dir"))

    if not storage.has_node_identity():
        raise click.ClickException("No identity configured. Run: verisoo identity generate")

    # -------------------------------------------------------------------------
    # Load the attestor's private key
    # Optionally decrypt if password-protected
    # -------------------------------------------------------------------------
    key_password = None
    if password:
        key_password = click.prompt("Password", hide_input=True).encode()

    private_key = load_private_key(storage.private_key_path, key_password)

    # -------------------------------------------------------------------------
    # Read image file
    # -------------------------------------------------------------------------
    image_data = image.read_bytes()

    # -------------------------------------------------------------------------
    # Build metadata from CLI options
    # These will override any EXIF data with the same keys
    # -------------------------------------------------------------------------
    metadata: dict[str, Any] = {}

    # Tags (can specify multiple: -t news -t ukraine)
    if tag:
        metadata["tags"] = list(tag)

    # Always record the original filename
    metadata["filename"] = image.name

    # Parse and add location if provided via CLI
    # This OVERRIDES any GPS data from EXIF
    if location_str:
        metadata["location"] = _parse_location(location_str)

    # Add caption (photographer's notes)
    if caption:
        metadata["caption"] = caption

    # -------------------------------------------------------------------------
    # Create the attestation
    # This: computes hashes, extracts EXIF (if enabled), signs the record
    # -------------------------------------------------------------------------
    attestation = create_attestation(
        image_data, private_key, metadata, auto_exif=not no_exif
    )

    # -------------------------------------------------------------------------
    # Store the attestation record
    # Appends to binary log and updates LMDB index
    # -------------------------------------------------------------------------
    index = storage.append_record(attestation.record)

    # -------------------------------------------------------------------------
    # Update Merkle tree root
    # This provides tamper-evidence for the entire log
    # -------------------------------------------------------------------------
    merkle_log = storage.load_merkle_log()
    storage.set_merkle_root(merkle_log.root_hash)

    # -------------------------------------------------------------------------
    # Embed proof link if requested
    # -------------------------------------------------------------------------
    embed_result = None
    if embed:
        from .embed import embed_proof_link

        # Generate proof link from attestation record
        proof_link = f"{base_url}/v/{attestation.record.short_id}"

        embed_result = embed_proof_link(
            image_path=image,
            proof_link=proof_link,
            fingerprint=attestation.record.attestor_fingerprint,
            attested_at=attestation.record.timestamp,
            image_sha256=attestation.image_hashes.sha256,
            caption=caption,
        )

    # -------------------------------------------------------------------------
    # Output results
    # -------------------------------------------------------------------------
    if ctx.obj.get("json"):
        result = {
            "index": index,
            "sha256": attestation.image_hashes.sha256,
            "attestor": attestation.record.attestor_fingerprint,
            "timestamp": attestation.record.timestamp.isoformat(),
            "root": merkle_log.root_hash,
        }
        if embed_result:
            result["embed"] = {
                "success": embed_result.success,
                "method": embed_result.method,
                "output": str(embed_result.output_path) if embed_result.output_path else None,
                "proof_link": embed_result.proof_link,
                "message": embed_result.message,
            }
        click.echo(json.dumps(result))
    else:
        click.echo(f"Attested: {image.name}")
        click.echo(f"  SHA-256: {attestation.image_hashes.sha256[:16]}...")
        click.echo(f"  Index:   {index}")
        click.echo(f"  Root:    {merkle_log.root_hash[:16]}...")
        if embed_result:
            if embed_result.success:
                click.echo(f"  Embedded: {embed_result.output_path.name if embed_result.output_path else 'N/A'} ({embed_result.method.upper()})")
                click.echo(f"  Proof:    {embed_result.proof_link}")
            else:
                click.echo(f"  Embed failed: {embed_result.message}")


@main.command()
@click.argument("image", type=click.Path(exists=True, path_type=Path))
@click.option("--exact", is_flag=True, help="Require exact byte match (not perceptual)")
@click.pass_context
def verify(ctx: click.Context, image: Path, exact: bool) -> None:
    """Verify an image against known attestations."""
    from .hashing import hash_image
    from .verification import find_attestations_for_image
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))

    # Read image and compute hashes
    image_data = image.read_bytes()
    hashes = hash_image(image_data)

    # Find matching attestations
    records = list(storage.iterate_records())
    matches = find_attestations_for_image(
        image_data, records, perceptual_threshold=0 if exact else 10
    )

    if ctx.obj.get("json"):
        result = {
            "image": str(image),
            "sha256": hashes.sha256,
            "matches": len(matches),
            "attestations": [
                {
                    "attestor": m.attestor_fingerprint,
                    "timestamp": m.timestamp.isoformat(),
                    "exact": m.image_hashes.sha256 == hashes.sha256,
                }
                for m in matches
            ],
        }
        click.echo(json.dumps(result))
    else:
        if not matches:
            click.echo(f"No attestations found for {image.name}")
            click.echo(f"  SHA-256: {hashes.sha256[:16]}...")
            sys.exit(1)

        click.echo(f"Found {len(matches)} attestation(s) for {image.name}")
        for m in matches:
            match_type = "exact" if m.image_hashes.sha256 == hashes.sha256 else "perceptual"
            click.echo(f"  [{match_type}] {m.attestor_fingerprint[:16]}... @ {m.timestamp.isoformat()}")


# --- Log commands ---


@main.group()
def log() -> None:
    """Query the attestation log."""
    pass


@log.command("status")
@click.pass_context
def log_status(ctx: click.Context) -> None:
    """Show log status (size, root hash)."""
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))
    stats = storage.get_stats()

    if ctx.obj.get("json"):
        click.echo(json.dumps({
            "records": stats.record_count,
            "root": stats.merkle_root,
            "log_size_bytes": stats.log_file_size,
            "unique_images": stats.unique_images_sha256,
            "attestors": stats.attestor_count,
        }))
    else:
        click.echo(f"Records:       {stats.record_count}")
        click.echo(f"Root:          {stats.merkle_root or '(empty)'}")
        click.echo(f"Log size:      {stats.log_file_size:,} bytes")
        click.echo(f"Unique images: {stats.unique_images_sha256}")
        click.echo(f"Attestors:     {stats.attestor_count}")


@log.command("list")
@click.option("--limit", "-n", default=10, help="Number of records to show")
@click.pass_context
def log_list(ctx: click.Context, limit: int) -> None:
    """List recent attestations."""
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))

    records = list(storage.iterate_records())
    recent = records[-limit:] if len(records) > limit else records

    if ctx.obj.get("json"):
        click.echo(json.dumps([
            {
                "sha256": r.image_hashes.sha256,
                "attestor": r.attestor_fingerprint,
                "timestamp": r.timestamp.isoformat(),
            }
            for r in recent
        ]))
    else:
        for r in recent:
            click.echo(f"{r.timestamp.isoformat()} {r.attestor_fingerprint[:16]}... {r.image_hashes.sha256[:16]}...")


# --- Peer commands ---


@main.group()
def peer() -> None:
    """Manage federation peers."""
    pass


@peer.command("add")
@click.argument("url")
@click.argument("fingerprint")
@click.pass_context
def peer_add(ctx: click.Context, url: str, fingerprint: str) -> None:
    """Add a federation peer."""
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))
    peers = storage.load_peers()

    # Check for duplicates
    if any(p["url"] == url for p in peers):
        raise click.ClickException(f"Peer already exists: {url}")

    peers.append({
        "url": url,
        "fingerprint": fingerprint,
        "added": datetime.now(timezone.utc).isoformat(),
    })
    storage.save_peers(peers)

    click.echo(f"Added peer: {url}")


@peer.command("list")
@click.pass_context
def peer_list(ctx: click.Context) -> None:
    """List federation peers."""
    from .storage import LocalStorage

    storage = LocalStorage(ctx.obj.get("data_dir"))
    peers = storage.load_peers()

    if ctx.obj.get("json"):
        click.echo(json.dumps(peers))
    else:
        if not peers:
            click.echo("No peers configured")
        else:
            for p in peers:
                click.echo(f"{p['url']} ({p['fingerprint'][:16]}...)")


# =============================================================================
# API SERVER COMMAND
# =============================================================================
#
# The `serve` command launches a FastAPI-based HTTP server for:
#   - Remote image verification (POST /verify)
#   - Proof lookup by ID (GET /v/{short_id})
#   - Status and health checks
#
# This enables integration with:
#   - Newsroom editorial systems
#   - Fact-checking workflows
#   - Mobile apps
#   - Browser-based verification widgets
#
# The server reads from the local attestation log - it doesn't create
# attestations (that requires the private key, which should stay local).
# =============================================================================


@main.command()
@click.option("--host", default="0.0.0.0", help="Host to bind to")
@click.option("--port", default=8000, type=int, help="Port to listen on")
def serve(host: str, port: int) -> None:
    """
    Run the Verisoo verification API server.

    Starts a FastAPI server that exposes verification endpoints. This allows
    remote clients to verify images against your local attestation log.

    \b
    ENDPOINTS:
        POST /verify          Upload image for verification
        GET  /v/{short_id}    Get proof by short ID
        GET  /status          Server status and statistics
        GET  /health          Health check for load balancers

    \b
    EXAMPLES:
        # Start on default port
        verisoo serve

        # Custom port
        verisoo serve --port 9000

        # Bind to localhost only (no external access)
        verisoo serve --host 127.0.0.1

    \b
    CLIENT USAGE:
        # Verify an image
        curl -X POST -F "image=@photo.jpg" http://localhost:8000/verify

        # Get proof details
        curl http://localhost:8000/v/a8f3c2d1e9b7

    \b
    ENVIRONMENT VARIABLES:
        VERISOO_DATA_DIR    Override data directory
        VERISOO_BASE_URL    Base URL for proof links (default: https://verisoo.io)

    \b
    SECURITY NOTES:
        - The server is READ-ONLY (cannot create attestations)
        - CORS is enabled by default (configure for production)
        - No authentication by default (add API keys for production)
    """
    # -------------------------------------------------------------------------
    # Import the API module (requires fastapi, uvicorn)
    # These are optional dependencies: pip install verisoo[api]
    # -------------------------------------------------------------------------
    try:
        from .api import serve as run_server
    except ImportError:
        raise click.ClickException("API server requires fastapi: pip install verisoo[api]")

    # -------------------------------------------------------------------------
    # Start the server
    # Uses uvicorn as the ASGI server
    # -------------------------------------------------------------------------
    click.echo(f"Starting Verisoo API server on {host}:{port}")
    click.echo("Press Ctrl+C to stop")
    run_server(host=host, port=port)


# =============================================================================
# ENTRY POINT
# =============================================================================

if __name__ == "__main__":
    main()
