#!/usr/bin/env python3
"""HCFS Client CLI — Python port of hcfs-client-cli.

Command-line interface for the Hippius Confidential File System.
"""

import argparse
import getpass
import os
import shutil
import sys

from hcfs_client import (
    Drive,
    HcfsClient,
    HcfsClientConfig,
    SyncConflictResolution,
    SyncConflictType,
)


def main():
    parser = argparse.ArgumentParser(
        prog="hcfs",
        description="Hippius Confidential File System CLI",
    )
    parser.add_argument("-p", "--path", help="Path to the sync directory (defaults to current directory)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
    parser.add_argument("--password", default=os.environ.get("HCFS_PASSWORD"), help="Password for non-interactive use (use HCFS_PASSWORD env var for better security)")

    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("init", help="Initialize a new HCFS drive")

    config_p = sub.add_parser("config", help="Configure server settings")
    config_p.add_argument("--server-url", required=True, help="Server URL")
    config_p.add_argument("--api-key", required=True, help="API key for server authentication")
    config_p.add_argument("--bearer-token", help="Bearer token (optional, auto-derived on unlock)")
    config_p.add_argument("--insecure", action="store_true", help="Accept invalid TLS certificates")
    config_p.add_argument("--billing-bypass-token", help="Billing bypass token (for testing)")
    config_p.add_argument("--account-ss58", help="SS58 address of user's Substrate/Bittensor account")

    sub.add_parser("stage", help="Stage files for syncing")

    sync_p = sub.add_parser("sync", help="Sync files with the server")
    sync_p.add_argument("--non-interactive", action="store_true", help="Auto-skip conflicts")

    dl_p = sub.add_parser("download", help="Download a single file by its file ID")
    dl_p.add_argument("--account-ss58", required=True, help="SS58 address of the file owner")
    dl_p.add_argument("--file-id", required=True, help="File ID (hex-encoded path hash)")
    dl_p.add_argument("-o", "--output", required=True, help="Output file path")

    args = parser.parse_args()
    drive_path = args.path or os.getcwd()

    try:
        if args.command == "init":
            cmd_init(drive_path, args.password)
        elif args.command == "config":
            cmd_config(
                drive_path,
                args.server_url,
                args.api_key,
                args.bearer_token,
                args.insecure,
                args.billing_bypass_token,
                args.account_ss58,
            )
        elif args.command == "stage":
            cmd_stage(drive_path)
        elif args.command == "sync":
            cmd_sync(drive_path, args.non_interactive, args.password)
        elif args.command == "download":
            cmd_download(drive_path, args.account_ss58, args.file_id, args.output)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


def prompt_password(prompt, preset=None):
    if preset is not None:
        return preset
    return getpass.getpass(prompt)


# =============================================================================
# Progress bars (no external dependencies)
# =============================================================================

def _format_bytes(n):
    for unit in ("B", "KB", "MB", "GB"):
        if n < 1024:
            return f"{n:.1f}{unit}" if unit != "B" else f"{n}{unit}"
        n /= 1024
    return f"{n:.1f}TB"


def _truncate_path(path, max_len=30):
    if path and len(path) > max_len:
        return "..." + path[-(max_len - 3):]
    return path or ""


def _make_progress_bar(icon):
    """Create a transfer progress callback that renders an inline progress bar."""
    state = {"last_total": 0}

    def callback(sent, total, path):
        if total == 0:
            return
        if total != state["last_total"]:
            state["last_total"] = total
        cols = shutil.get_terminal_size((80, 24)).columns
        label = f"{icon} {_truncate_path(path, 30)}"
        pct = sent / total if total else 0
        bar_width = max(10, cols - len(label) - 30)
        filled = int(bar_width * pct)
        bar = "\u2501" * filled + "\u2578" + " " * (bar_width - filled - 1)
        line = f"\r{label} [{bar}] {_format_bytes(sent)}/{_format_bytes(total)}"
        print(line, end="", flush=True)
        if sent >= total:
            print("\r" + " " * cols + "\r", end="", flush=True)

    return callback


def cmd_init(drive_path, preset_password=None):
    drive = Drive(drive_path)

    if drive.is_initialized():
        print(f"Drive already initialized at {drive_path}")
        return

    print(f"Initializing HCFS drive at {drive_path}")
    print()

    # Ask if user has an existing recovery phrase
    existing_mnemonic = None
    is_recovery = False
    if preset_password is None:
        existing_mnemonic = prompt_for_existing_mnemonic()
        is_recovery = existing_mnemonic is not None

    password = prompt_password("Enter password to encrypt your keys: ", preset_password)
    password_confirm = prompt_password("Confirm password: ", preset_password)

    if password != password_confirm:
        raise RuntimeError("Passwords do not match")

    if len(password) < 8:
        raise RuntimeError("Password must be at least 8 characters")

    mnemonic = drive.init(password, existing_mnemonic)

    print()
    if is_recovery:
        print("Drive recovered successfully!")
        print("Your recovery phrase has been encrypted and stored.")
    else:
        print("Drive initialized successfully!")
        print()
        print("IMPORTANT: Write down your recovery phrase and store it safely.")
        print("This is the ONLY way to recover your files if you lose your password.")
        print()
        print("Recovery phrase (24 words):")
        print("\u2500" * 65)
        print(mnemonic)
        print("\u2500" * 65)
    print()


def prompt_for_existing_mnemonic():
    try:
        answer = input("Do you have an existing recovery phrase to restore? [y/N]: ")
    except EOFError:
        return None

    if answer.strip().lower() not in ("y", "yes"):
        return None

    print()
    print("Enter your 24-word recovery phrase (space-separated):")
    print()

    while True:
        try:
            mnemonic = input("> ").strip()
        except EOFError:
            return None

        if not mnemonic:
            print("Recovery phrase cannot be empty. Please try again.")
            continue

        word_count = len(mnemonic.split())
        if word_count != 24:
            print(f"Expected 24 words, but got {word_count}. Please enter all 24 words.")
            continue

        return " ".join(mnemonic.split())


def cmd_config(drive_path, server_url, api_key, bearer_token, insecure, billing_bypass_token, account_ss58):
    drive = Drive(drive_path)

    if not drive.is_initialized():
        raise RuntimeError(f"No HCFS drive found at {drive_path}. Run 'hcfs init' first.")

    config = HcfsClientConfig(
        server_url,
        api_key,
        bearer_token or "",
        account_ss58 or "",
        insecure,
    )
    config.save(drive_path)

    print(f"Server configured: {server_url}")
    if account_ss58:
        print(f"Account SS58:      {account_ss58}")
    print("Note: Bearer token will be auto-derived from your keys on sync.")


def cmd_stage(drive_path):
    drive = Drive(drive_path)

    if not drive.is_initialized():
        raise RuntimeError(f"No HCFS drive found at {drive_path}. Run 'hcfs init' first.")

    plan, local_count, remote_count, synced_count, path_index = drive.stage()

    print(f"HCFS Staging: {drive_path}")
    print("\u2500" * 65)
    print(f"Local files:    {local_count}")
    print(f"Remote files:   {remote_count}")
    print(f"Synced files:   {synced_count}")
    print()

    def get_path(file_id_hex):
        return path_index.get(file_id_hex, f"<{file_id_hex[:16]}>")

    if not plan.has_work():
        print("Everything is in sync.")
        return

    if plan.uploads:
        print(f"To upload ({len(plan.uploads)}):")
        for fid in plan.uploads:
            print(f"  \u2191 {get_path(fid)}")
        print()

    if plan.downloads:
        print(f"To download ({len(plan.downloads)}):")
        for fid in plan.downloads:
            print(f"  \u2193 {get_path(fid)}")
        print()

    if plan.local_deletes:
        print(f"To delete locally ({len(plan.local_deletes)}):")
        for fid in plan.local_deletes:
            print(f"  \u2715 {get_path(fid)}")
        print()

    if plan.remote_deletes:
        print(f"To delete remotely ({len(plan.remote_deletes)}):")
        for fid in plan.remote_deletes:
            print(f"  \u2715 {get_path(fid)}")
        print()

    if plan.conflicts:
        print(f"Conflicts ({len(plan.conflicts)}):")
        for conflict in plan.conflicts:
            path = get_path(conflict.path_hash)
            ct = conflict.conflict_type
            if ct == SyncConflictType.ModifyModify:
                desc = "both modified"
            elif ct == SyncConflictType.ModifyDelete:
                desc = "local modified, remote deleted"
            elif ct == SyncConflictType.DeleteModify:
                desc = "local deleted, remote modified"
            elif ct == SyncConflictType.CreateCreate:
                desc = "both created"
            else:
                desc = "unknown"
            print(f"  \u26a0 {path} ({desc})")
        print()


def cmd_sync(drive_path, non_interactive, preset_password=None):
    drive = Drive(drive_path)

    if not drive.is_initialized():
        raise RuntimeError(f"No HCFS drive found at {drive_path}. Run 'hcfs init' first.")

    config = HcfsClientConfig.load(drive_path)
    if config is None:
        raise RuntimeError("Server not configured. Run 'hcfs config' first.")

    drive.set_config(config)

    password = prompt_password("Enter password: ", preset_password)
    drive.unlock(password)

    print(f"Syncing with {config.base_url}...")
    print()

    drive.set_progress_handlers(
        on_upload=_make_progress_bar("\u2191"),
        on_download=_make_progress_bar("\u2193"),
    )

    if non_interactive:
        outcome = drive.sync()
    else:
        outcome = drive.sync(resolve_conflict)

    drive.clear_progress_handlers()

    print()
    print("Sync complete:")
    print(f"  Uploaded:          {outcome.files_uploaded} file(s)")
    print(f"  Downloaded:        {outcome.files_downloaded} file(s)")
    print(f"  Deleted locally:   {outcome.files_deleted_locally} file(s)")
    print(f"  Deleted remotely:  {outcome.files_deleted_remotely} file(s)")

    if outcome.conflicts_resolved > 0 or outcome.conflicts_skipped > 0:
        print(f"  Conflicts resolved: {outcome.conflicts_resolved}")
        print(f"  Conflicts skipped:  {outcome.conflicts_skipped}")


def resolve_conflict(conflict):
    """Prompt user to resolve a sync conflict. Matches Rust CLI behavior."""
    if conflict.kind == "upload":
        c = conflict.upload
        path_display = c.local_path or f"<{c.file_id[:16]}>"

        print()
        print("\u2501" * 69)
        print(f"  UPLOAD CONFLICT: {path_display}")
        print("\u2501" * 69)
        print()
        print(f"  {c.message}")
        print()
        print(f"  Server revision: {c.server_revision_id[:16]}")
        if c.base_revision_id:
            print(f"  Your base:       {c.base_revision_id[:16]}")
        print()

    elif conflict.kind == "plan":
        c = conflict.plan
        path_display = c.local_path or f"<{c.file_id[:16]}>"

        conflict_desc = {
            SyncConflictType.ModifyModify: "Both local and remote modified this file differently",
            SyncConflictType.ModifyDelete: "You modified this file, but it was deleted on the server",
            SyncConflictType.DeleteModify: "You deleted this file, but it was modified on the server",
            SyncConflictType.CreateCreate: "Both local and remote created this file with different content",
        }.get(c.conflict_type, "Unknown conflict")

        print()
        print("\u2501" * 69)
        print(f"  SYNC CONFLICT: {path_display}")
        print("\u2501" * 69)
        print()
        print(f"  Type: {c.conflict_type}")
        print(f"  {conflict_desc}")
        print()
        print(f"  Local exists:  {'Yes' if c.has_local else 'No (deleted)'}")
        print(f"  Remote exists: {'Yes' if c.has_remote else 'No (deleted)'}")
        print()

    return prompt_resolution_choice()


def prompt_resolution_choice():
    """Common resolution choice prompt. Matches Rust CLI behavior."""
    print("  Options:")
    print("    [L] Keep LOCAL  - Use your local version")
    print("    [R] Keep REMOTE - Use the server version")
    print("    [B] Keep BOTH   - Save local as .conflict file, use remote")
    print("    [S] Skip        - Handle this conflict later")
    print()

    while True:
        try:
            choice = input("  Your choice [L/R/B/S]: ").strip().upper()
        except EOFError:
            print("  \u2192 Skipping for now")
            return SyncConflictResolution.Skip

        if choice == "L":
            print("  \u2192 Keeping local version")
            return SyncConflictResolution.KeepLocal
        elif choice == "R":
            print("  \u2192 Accepting remote version")
            return SyncConflictResolution.AcceptRemote
        elif choice == "B":
            print("  \u2192 Keeping both versions")
            return SyncConflictResolution.KeepBoth
        elif choice in ("S", ""):
            print("  \u2192 Skipping for now")
            return SyncConflictResolution.Skip
        else:
            print("  Invalid choice. Please enter L, R, B, or S.")


def cmd_download(drive_path, account_ss58, file_id, output):
    config = HcfsClientConfig.load(drive_path)
    if config is None:
        raise RuntimeError("Server not configured. Run 'hcfs config' first.")

    client = HcfsClient(config)

    print(f"Downloading file {file_id}...")

    def download_progress(received, total):
        if total == 0:
            return
        cols = shutil.get_terminal_size((80, 24)).columns
        label = "\u2193 Downloading"
        pct = received / total if total else 0
        bar_width = max(10, cols - len(label) - 30)
        filled = int(bar_width * pct)
        bar = "\u2501" * filled + "\u2578" + " " * (bar_width - filled - 1)
        line = f"\r{label} [{bar}] {_format_bytes(received)}/{_format_bytes(total)}"
        print(line, end="", flush=True)
        if received >= total:
            print("\r" + " " * cols + "\r", end="", flush=True)

    result = client.download(account_ss58, file_id, output, download_progress)

    print("Download complete!")
    print(f"  File ID:      {file_id}")
    print(f"  Output:       {output}")
    print(f"  Size:         {result.size_bytes} bytes")
    print(f"  Revision seq: {result.revision_seq}")
    print(f"  Revision ID:  {result.revision_id[:16]}")


if __name__ == "__main__":
    main()
