use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;

use mftp::transfer::{receiver, sender};
use mftp::transfer::sender::ForcedTransport;

/// Transport path for `--transport`.
#[derive(clap::ValueEnum, Clone)]
enum Transport {
    /// QUIC with BBR congestion control (default when omitted in auto mode).
    /// Fails immediately if QUIC is unreachable — no TCP+TLS or SFTP fallback.
    Quic,
    /// TCP+TLS. Skip the QUIC probe. No SFTP fallback.
    Tcp,
    /// Parallel SFTP through SSH port 22 (SSH mode only, ~22 MiB/s cap).
    /// Skips the remote mftp server launch entirely.
    Sftp,
}

#[derive(Parser)]
#[command(name = "mftp", about = "High-throughput file transfer over high-latency links", version)]
struct Cli {
    #[command(subcommand)]
    command: Command,

    /// Number of parallel streams.
    /// Direct mode (QUIC/TCP): default auto-negotiated from RTT + CPU cores.
    /// SFTP fallback: default 8 (each stream is one SSH/SFTP connection;
    /// scales linearly — raise to 12 if the remote sshd allows it).
    #[arg(short = 'n', long, global = true, value_name = "N")]
    streams: Option<usize>,

    /// Chunk size in bytes (default: auto-negotiated from RTT)
    #[arg(long, global = true)]
    chunk_size: Option<usize>,

    /// Disable adaptive zstd compression
    #[arg(long, global = true)]
    no_compress: bool,

    /// Force a specific transport path.
    ///
    /// quic — QUIC only; fails immediately if UDP is blocked (no TCP or SFTP fallback).
    ///
    /// tcp  — TCP+TLS only; skip the QUIC probe (no SFTP fallback).
    ///
    /// sftp — parallel SFTP through SSH port 22 (SSH mode only; ~22 MiB/s cap).
    ///        Skips the remote mftp server launch — port 22 is all that's needed.
    ///
    /// Omit to use auto mode: QUIC → TCP+TLS → SFTP (SSH mode only).
    #[arg(long, global = true, value_name = "TRANSPORT")]
    transport: Option<Transport>,

    /// Force TCP+TLS transport (alias for --transport tcp)
    #[arg(long, global = true, hide = true)]
    tcp: bool,

    /// In auto mode, switch to TCP+TLS when measured RTT is at or below this value (ms).
    /// Default 5 ms: QUIC+BBR is slower than TCP+CUBIC on LAN due to slow congestion
    /// window ramp-up.  Set 0 to always use QUIC, or higher to widen the TCP window.
    /// Ignored when --transport is set.
    #[arg(long, global = true, default_value = "5", value_name = "MS")]
    tcp_below_rtt: f64,

    /// Verbosity (-v = info, -vv = debug, -vvv = trace)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    verbose: u8,
}

#[derive(Subcommand)]
enum Command {
    /// Send a file to a remote host
    Send {
        /// File to send
        file: std::path::PathBuf,
        /// Where to send the file.
        ///
        /// `host:port`          — connect to an already-running `mftp receive`.
        ///
        /// `[user@]host:/path`  — SSH mode: mftp launches a one-shot receiver
        /// on the remote automatically. Falls back through three transports:
        /// QUIC (UDP, needs open port) → TCP+TLS (TCP, needs open port) →
        /// SFTP (port 22 only, ~22 MiB/s cap). Use --port to specify a
        /// firewall-allowed port; without it a random port is used and QUIC/
        /// TCP+TLS will likely fall back to SFTP.
        destination: String,
        /// Pin the receiver's certificate fingerprint (hex SHA-256).
        /// Omit to use TOFU: fingerprint is printed and you are prompted once per
        /// session (requires a TTY; non-interactive use without --trust is rejected).
        /// Fingerprints are not stored between sessions — pass --trust in scripts.
        /// Ignored in SSH mode — fingerprint is obtained automatically.
        #[arg(long)]
        trust: Option<String>,
        /// Path to a pre-installed mftp binary on the remote (SSH mode only).
        /// By default mftp pipes itself over SSH stdin and caches it at
        /// ~/.cache/mftp-<hash> on the remote; subsequent transfers with the
        /// same binary version skip the copy.
        #[arg(long)]
        remote_mftp: Option<String>,
        /// Port for the remote mftp server to listen on (SSH mode only).
        /// Defaults to a randomly assigned port. Useful when the data-transfer
        /// port must be in a firewall allow-list.
        #[arg(long)]
        port: Option<u16>,
    },
    /// Receive files (run as server)
    Receive {
        /// Address to listen on
        #[arg(default_value = "0.0.0.0:7777")]
        bind: String,
        /// Directory to write received files into
        #[arg(short, long, default_value = ".")]
        output_dir: std::path::PathBuf,
    },
    /// One-shot server mode launched by the sender via SSH (not for direct use)
    #[command(hide = true)]
    Server {
        /// Directory to write received files into
        #[arg(short, long, default_value = ".")]
        output_dir: std::path::PathBuf,
        /// Port to listen on (default: random)
        #[arg(long)]
        port: Option<u16>,
    },
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();

    let filter = match cli.verbose {
        0 => "warn",
        1 => "info",
        2 => "debug",
        _ => "trace",
    };
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::new(filter))
        .init();

    match cli.command {
        Command::Send { file, destination, trust, remote_mftp, port } => {
            let tcp_rtt_threshold = std::time::Duration::from_secs_f64(cli.tcp_below_rtt / 1000.0);
            let forced_transport = match (cli.transport, cli.tcp) {
                (Some(Transport::Quic), _) => Some(ForcedTransport::Quic),
                (Some(Transport::Tcp), _) | (None, true) => Some(ForcedTransport::Tcp),
                (Some(Transport::Sftp), _) => Some(ForcedTransport::Sftp),
                (None, false) => None,
            };
            let config = sender::SendConfig {
                streams: cli.streams,
                chunk_size: cli.chunk_size,
                compress: !cli.no_compress,
                compress_level: 3,
                trusted_fingerprint: trust,
                forced_transport,
                tcp_rtt_threshold,
            };
            if let Some(dest) = mftp::ssh::parse_ssh_dest(&destination) {
                mftp::ssh::send_via_ssh(file, dest, config, remote_mftp, port).await
            } else {
                let addr = destination
                    .parse()
                    .with_context(|| format!("invalid address: {destination}"))?;
                sender::send(file, addr, config).await
            }
        }
        Command::Receive { bind, output_dir } => {
            let addr = bind
                .parse()
                .with_context(|| format!("invalid bind address: {bind}"))?;
            if cli.tcp {
                receiver::listen_tcp(addr, receiver::ReceiveConfig { output_dir }).await
            } else {
                receiver::listen(addr, receiver::ReceiveConfig { output_dir }).await
            }
        }
        Command::Server { output_dir, port } => {
            receiver::serve_one_stdio(output_dir, port).await
        }
    }
}
