#!/usr/bin/env python3
"""
FibPi Zlib CLI - Random-Access Compression Commands

Provides command-line interface for FibPi chunked compression with
random-access capability.

Commands:
    compress    - Compress a file with per-chunk zlib streams
    decompress  - Fully decompress a file
    range       - Extract a byte range (random access)
    info        - Show file metadata and chunk info
    verify      - Verify integrity of compressed file
    benchmark   - Compare performance vs standard gzip

Usage:
    # Native FibPi format (full features)
    fibpi-zlib compress large_file.bin -o large_file.fibpi.gz

    # Gzip-compatible mode (works with standard gunzip)
    fibpi-zlib compress large_file.bin -o large_file.gz --compat

    # Random access extraction
    fibpi-zlib range large_file.fibpi.gz 1000000 2000000 > slice.bin

    # Full decompress
    fibpi-zlib decompress large_file.fibpi.gz -o large_file.bin

    # Benchmark
    fibpi-zlib benchmark large_file.bin
"""

import argparse
import sys
import os
import time
import json
import hashlib
from pathlib import Path
from typing import Optional

# Import our modules
try:
    from .fibpi_zlib_wrapper import FibPiZlibWrapper, FibPiZlibIndex, MAGIC
    from .fibpi_gzip_compat import (
        GzipCompatibleFibPi, SidecarIndex, ConcatenatedGzipReader,
        create_sidecar_from_concatenated_gzip
    )
except ImportError:
    # Allow running as standalone script
    from fibpi_zlib_wrapper import FibPiZlibWrapper, FibPiZlibIndex, MAGIC
    from fibpi_gzip_compat import (
        GzipCompatibleFibPi, SidecarIndex, ConcatenatedGzipReader,
        create_sidecar_from_concatenated_gzip
    )


def sha256_hex(data: bytes) -> str:
    """Compute SHA256 as hex string."""
    return hashlib.sha256(data).hexdigest()


def format_size(size: int) -> str:
    """Format byte size in human-readable form."""
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if size < 1024:
            return f"{size:.2f} {unit}"
        size /= 1024
    return f"{size:.2f} PB"


def detect_format(filepath: Path) -> str:
    """
    Detect the format of a compressed file.

    Returns:
        'fibpi': Native FibPi-zlib format
        'gzip_sidecar': Gzip with sidecar index
        'gzip': Standard gzip (no sidecar)
        'unknown': Unknown format
    """
    with open(filepath, 'rb') as f:
        header = f.read(20)

    if header.startswith(MAGIC):
        return 'fibpi'

    if header[:2] == b'\x1f\x8b':  # gzip magic
        sidecar = filepath.with_suffix(filepath.suffix + '.fibpi.idx')
        if sidecar.exists():
            return 'gzip_sidecar'
        return 'gzip'

    return 'unknown'


def cmd_compress(args):
    """Compress a file."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    # Determine output path
    if args.output:
        output_path = Path(args.output)
    else:
        if args.compat:
            output_path = input_path.with_suffix('.gz')
        else:
            output_path = input_path.with_suffix('.fibpi.gz')

    # Read input
    print(f"Reading: {input_path}")
    with open(input_path, 'rb') as f:
        data = f.read()

    original_size = len(data)
    print(f"  Size: {format_size(original_size)} ({original_size:,} bytes)")

    if args.compat:
        # Gzip-compatible mode with sidecar
        print(f"\nCompressing (gzip-compatible mode)...")
        compat = GzipCompatibleFibPi(compression_level=args.level)
        gz_path, idx_path = compat.write_gzip_with_index(
            data, output_path, source_filename=input_path.name
        )

        gz_size = gz_path.stat().st_size
        idx_size = idx_path.stat().st_size

        print(f"\nOutput:")
        print(f"  {gz_path}")
        print(f"    Size: {format_size(gz_size)} ({gz_size:,} bytes)")
        print(f"    Ratio: {gz_size/original_size:.2%}")
        print(f"  {idx_path} (sidecar index)")
        print(f"    Size: {format_size(idx_size)} ({idx_size:,} bytes)")
        print(f"\nNote: {gz_path.name} is standard gzip - 'gunzip' will work")
    else:
        # Native FibPi format
        print(f"\nCompressing (native FibPi format)...")
        wrapper = FibPiZlibWrapper(compression_level=args.level)
        result = wrapper.compress_file(input_path, output_path)

        print(f"\nOutput: {output_path}")
        print(f"  Compressed: {format_size(result['compressed_size'])} ({result['compressed_size']:,} bytes)")
        print(f"  Ratio: {result['ratio']:.2%}")
        print(f"  Chunks: {result['num_chunks']}")
        print(f"  SHA256: {result['original_sha256']}")

    return 0


def cmd_decompress(args):
    """Decompress a file."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    fmt = detect_format(input_path)

    # Determine output path
    if args.output:
        output_path = Path(args.output)
    else:
        if input_path.suffix == '.gz':
            output_path = input_path.with_suffix('')
            if str(output_path).endswith('.fibpi'):
                output_path = Path(str(output_path)[:-6])
        else:
            output_path = input_path.with_suffix('.decompressed')

    print(f"Input: {input_path}")
    print(f"Format: {fmt}")

    if fmt == 'fibpi':
        wrapper = FibPiZlibWrapper()
        result = wrapper.decompress_file(
            input_path, output_path, verify=not args.no_verify
        )
        print(f"\nOutput: {output_path}")
        print(f"  Size: {format_size(result['decompressed_size'])}")
        print(f"  SHA256: {result['sha256']}")

    elif fmt == 'gzip_sidecar':
        compat = GzipCompatibleFibPi()
        data = compat.full_decompress_with_index(
            input_path, verify=not args.no_verify
        )
        with open(output_path, 'wb') as f:
            f.write(data)
        print(f"\nOutput: {output_path}")
        print(f"  Size: {format_size(len(data))}")
        print(f"  SHA256: {sha256_hex(data)}")

    elif fmt == 'gzip':
        import gzip
        print("Warning: No sidecar index found - using standard gzip decompress")
        with gzip.open(input_path, 'rb') as f:
            data = f.read()
        with open(output_path, 'wb') as f:
            f.write(data)
        print(f"\nOutput: {output_path}")
        print(f"  Size: {format_size(len(data))}")

    else:
        print(f"Error: Unknown format", file=sys.stderr)
        return 1

    return 0


def cmd_range(args):
    """Extract a byte range (random access)."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    fmt = detect_format(input_path)
    start = args.start
    end = args.end

    if start >= end:
        print(f"Error: Start ({start}) must be less than end ({end})", file=sys.stderr)
        return 1

    if fmt == 'fibpi':
        print(f"Extracting range [{start}, {end}) from FibPi file...", file=sys.stderr)
        wrapper = FibPiZlibWrapper()
        data = wrapper.extract_range_from_file(
            input_path, start, end, verify=not args.no_verify
        )

    elif fmt == 'gzip_sidecar':
        print(f"Extracting range [{start}, {end}) with sidecar index...", file=sys.stderr)
        compat = GzipCompatibleFibPi()
        data = compat.read_range_with_index(
            input_path, start, end, verify=not args.no_verify
        )

    else:
        print(f"Error: Random access requires FibPi format or gzip with sidecar", file=sys.stderr)
        return 1

    print(f"Extracted {len(data):,} bytes", file=sys.stderr)

    # Output to stdout or file
    if args.output:
        with open(args.output, 'wb') as f:
            f.write(data)
        print(f"Written to: {args.output}", file=sys.stderr)
    else:
        sys.stdout.buffer.write(data)

    return 0


def cmd_info(args):
    """Show file metadata and chunk info."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    fmt = detect_format(input_path)
    print(f"File: {input_path}")
    print(f"Format: {fmt}")
    print(f"File size: {format_size(input_path.stat().st_size)}")

    if fmt == 'fibpi':
        index = FibPiZlibIndex.from_file(input_path)
        meta = index.get_metadata()

        print(f"\nOriginal size: {format_size(meta['original_size'])}")
        print(f"Number of chunks: {meta['num_chunks']}")

        if args.verbose:
            print(f"\nChunk details:")
            chunks = index.list_chunks()
            for c in chunks[:20]:  # Limit to first 20
                print(f"  #{c['id']}: offset={c['decompressed_offset']:,}, "
                      f"len={c['decompressed_len']:,}, "
                      f"compressed={c['compressed_len']:,}")
            if len(chunks) > 20:
                print(f"  ... and {len(chunks) - 20} more chunks")

    elif fmt == 'gzip_sidecar':
        sidecar_path = input_path.with_suffix(input_path.suffix + '.fibpi.idx')
        with open(sidecar_path, 'rb') as f:
            index = SidecarIndex.from_bytes(f.read())

        print(f"\nSidecar: {sidecar_path}")
        print(f"Original size: {format_size(index.original_size)}")
        print(f"Original SHA256: {index.original_sha256.hex()}")
        print(f"Number of chunks: {index.num_chunks}")
        print(f"Created: {index.timestamp}")

        if args.verbose:
            print(f"\nChunk details:")
            for i, e in enumerate(index.entries[:20]):
                print(f"  #{e.chunk_id}: offset={e.original_offset:,}, "
                      f"len={e.original_len:,}, gz_len={e.gzip_len:,}")
            if len(index.entries) > 20:
                print(f"  ... and {len(index.entries) - 20} more chunks")

    elif fmt == 'gzip':
        # Try to scan for concatenated streams
        reader = ConcatenatedGzipReader(input_path)
        count = reader.get_stream_count()
        print(f"\nGzip streams: {count}")
        print("Note: No sidecar index - random access not available")

    return 0


def cmd_verify(args):
    """Verify integrity of compressed file."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    fmt = detect_format(input_path)
    print(f"Verifying: {input_path}")
    print(f"Format: {fmt}")

    if fmt == 'fibpi':
        wrapper = FibPiZlibWrapper()
        with open(input_path, 'rb') as f:
            compressed = f.read()

        try:
            data = wrapper.decompress(compressed, verify=True)
            print(f"\nStatus: OK")
            print(f"  Original size: {len(data):,} bytes")
            print(f"  SHA256: {sha256_hex(data)}")
            return 0
        except ValueError as e:
            print(f"\nStatus: FAILED")
            print(f"  Error: {e}")
            return 1

    elif fmt == 'gzip_sidecar':
        compat = GzipCompatibleFibPi()
        result = compat.verify_file(input_path)

        print(f"\nStatus: {result['overall_status']}")
        print(f"  Original size: {result['original_size']:,} bytes")
        print(f"  Chunks verified: {result['num_chunks']}")

        if result['overall_status'] != 'OK':
            for c in result['chunks']:
                if c['status'] != 'OK':
                    print(f"  Chunk {c['chunk_id']}: {c['status']}")
            return 1
        return 0

    else:
        print(f"Error: Cannot verify this format", file=sys.stderr)
        return 1


def cmd_benchmark(args):
    """Benchmark FibPi vs standard gzip."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    print(f"Benchmarking: {input_path}")
    print(f"File size: {format_size(input_path.stat().st_size)}")
    print("=" * 60)

    # Read input
    with open(input_path, 'rb') as f:
        data = f.read()

    original_size = len(data)

    # Standard gzip
    import gzip
    import tempfile

    print("\n1. Standard gzip compression:")
    t0 = time.time()
    gzip_compressed = gzip.compress(data, compresslevel=args.level)
    gzip_compress_time = time.time() - t0

    print(f"   Compressed: {format_size(len(gzip_compressed))} ({len(gzip_compressed)/original_size:.2%})")
    print(f"   Time: {gzip_compress_time:.3f}s")

    # FibPi native
    print("\n2. FibPi native compression:")
    wrapper = FibPiZlibWrapper(compression_level=args.level)
    t0 = time.time()
    fibpi_compressed = wrapper.compress_chunked(data)
    fibpi_compress_time = time.time() - t0

    print(f"   Compressed: {format_size(len(fibpi_compressed))} ({len(fibpi_compressed)/original_size:.2%})")
    print(f"   Time: {fibpi_compress_time:.3f}s")
    print(f"   Chunks: {wrapper.get_stats()['chunks_compressed']}")

    # FibPi gzip-compatible
    print("\n3. FibPi gzip-compatible compression:")
    compat = GzipCompatibleFibPi(compression_level=args.level)
    t0 = time.time()
    with tempfile.NamedTemporaryFile(suffix='.gz', delete=False) as tf:
        compat_path = Path(tf.name)
    gz_path, idx_path = compat.write_gzip_with_index(data, compat_path)
    compat_compress_time = time.time() - t0

    gz_size = gz_path.stat().st_size
    idx_size = idx_path.stat().st_size

    print(f"   .gz: {format_size(gz_size)} ({gz_size/original_size:.2%})")
    print(f"   .idx: {format_size(idx_size)}")
    print(f"   Total: {format_size(gz_size + idx_size)}")
    print(f"   Time: {compat_compress_time:.3f}s")

    # Decompression benchmark
    print("\n" + "=" * 60)
    print("Decompression benchmarks:")

    # Full decompress
    print("\n4. Full decompression:")

    t0 = time.time()
    gzip.decompress(gzip_compressed)
    gzip_decomp_time = time.time() - t0
    print(f"   Standard gzip: {gzip_decomp_time:.3f}s")

    t0 = time.time()
    wrapper.decompress(fibpi_compressed)
    fibpi_decomp_time = time.time() - t0
    print(f"   FibPi native: {fibpi_decomp_time:.3f}s")

    t0 = time.time()
    compat.full_decompress_with_index(gz_path, verify=False)
    compat_decomp_time = time.time() - t0
    print(f"   FibPi gzip-compat: {compat_decomp_time:.3f}s")

    # Random access benchmark
    print("\n5. Random access (1MB from middle of file):")

    mid = original_size // 2
    range_start = mid
    range_end = min(mid + 1024 * 1024, original_size)

    print(f"   Range: [{range_start:,}, {range_end:,})")

    # Standard gzip (must decompress all)
    t0 = time.time()
    full_data = gzip.decompress(gzip_compressed)
    _ = full_data[range_start:range_end]
    gzip_range_time = time.time() - t0
    print(f"   Standard gzip (full decompress): {gzip_range_time:.3f}s")

    # FibPi native
    t0 = time.time()
    _ = wrapper.decompress_range(fibpi_compressed, range_start, range_end, verify=False)
    fibpi_range_time = time.time() - t0
    print(f"   FibPi native: {fibpi_range_time:.3f}s ({gzip_range_time/fibpi_range_time:.1f}x faster)")

    # FibPi gzip-compat
    t0 = time.time()
    _ = compat.read_range_with_index(gz_path, range_start, range_end, verify=False)
    compat_range_time = time.time() - t0
    print(f"   FibPi gzip-compat: {compat_range_time:.3f}s ({gzip_range_time/compat_range_time:.1f}x faster)")

    # Cleanup
    gz_path.unlink()
    idx_path.unlink()

    print("\n" + "=" * 60)
    print("Summary:")
    print(f"  Compression overhead: +{(fibpi_compress_time/gzip_compress_time - 1)*100:.1f}%")
    print(f"  Random access speedup: {gzip_range_time/fibpi_range_time:.1f}x")

    return 0


def cmd_create_sidecar(args):
    """Create sidecar index for existing concatenated gzip."""
    input_path = Path(args.input)

    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        return 1

    fmt = detect_format(input_path)
    if fmt not in ('gzip', 'gzip_sidecar'):
        print(f"Error: Input must be a gzip file", file=sys.stderr)
        return 1

    output_path = args.output
    if output_path:
        output_path = Path(output_path)

    print(f"Creating sidecar index for: {input_path}")
    sidecar = create_sidecar_from_concatenated_gzip(input_path, output_path)
    print(f"Created: {sidecar}")
    print(f"Size: {format_size(sidecar.stat().st_size)}")

    return 0


def main():
    parser = argparse.ArgumentParser(
        description='FibPi Zlib - Random-Access Compression',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Compress to native FibPi format
  fibpi-zlib compress large_file.bin -o large_file.fibpi.gz

  # Compress to gzip-compatible format (works with gunzip)
  fibpi-zlib compress large_file.bin -o large_file.gz --compat

  # Extract byte range 5GB-5.001GB (random access)
  fibpi-zlib range huge.fibpi.gz 5368709120 5369757696 > slice.bin

  # Full decompress
  fibpi-zlib decompress large_file.fibpi.gz -o large_file.bin

  # Benchmark
  fibpi-zlib benchmark testfile.bin
"""
    )

    subparsers = parser.add_subparsers(dest='command', help='Command')

    # compress
    p_compress = subparsers.add_parser('compress', help='Compress a file')
    p_compress.add_argument('input', help='Input file')
    p_compress.add_argument('-o', '--output', help='Output file')
    p_compress.add_argument('--compat', action='store_true',
                           help='Gzip-compatible mode (creates .gz + .fibpi.idx)')
    p_compress.add_argument('-l', '--level', type=int, default=9,
                           help='Compression level 1-9 (default: 9)')

    # decompress
    p_decompress = subparsers.add_parser('decompress', help='Decompress a file')
    p_decompress.add_argument('input', help='Input file')
    p_decompress.add_argument('-o', '--output', help='Output file')
    p_decompress.add_argument('--no-verify', action='store_true',
                             help='Skip SHA256 verification')

    # range
    p_range = subparsers.add_parser('range', help='Extract byte range (random access)')
    p_range.add_argument('input', help='Input file')
    p_range.add_argument('start', type=int, help='Start byte offset')
    p_range.add_argument('end', type=int, help='End byte offset')
    p_range.add_argument('-o', '--output', help='Output file (default: stdout)')
    p_range.add_argument('--no-verify', action='store_true',
                        help='Skip SHA256 verification')

    # info
    p_info = subparsers.add_parser('info', help='Show file info')
    p_info.add_argument('input', help='Input file')
    p_info.add_argument('-v', '--verbose', action='store_true',
                       help='Show chunk details')

    # verify
    p_verify = subparsers.add_parser('verify', help='Verify file integrity')
    p_verify.add_argument('input', help='Input file')

    # benchmark
    p_bench = subparsers.add_parser('benchmark', help='Benchmark vs standard gzip')
    p_bench.add_argument('input', help='Input file to benchmark')
    p_bench.add_argument('-l', '--level', type=int, default=9,
                        help='Compression level 1-9 (default: 9)')

    # create-sidecar
    p_sidecar = subparsers.add_parser('create-sidecar',
                                      help='Create sidecar index for existing gzip')
    p_sidecar.add_argument('input', help='Input gzip file')
    p_sidecar.add_argument('-o', '--output', help='Output sidecar path')

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        return 1

    commands = {
        'compress': cmd_compress,
        'decompress': cmd_decompress,
        'range': cmd_range,
        'info': cmd_info,
        'verify': cmd_verify,
        'benchmark': cmd_benchmark,
        'create-sidecar': cmd_create_sidecar,
    }

    return commands[args.command](args)


if __name__ == '__main__':
    sys.exit(main())
