mod commands;
mod config;
mod remote;
mod repository;

use std::ffi::OsString;
use std::io::Write;
use std::path::PathBuf;

use clap::{AppSettings, Clap};
use human_panic::setup_panic;
use libpijul::pristine::Base32;
use thiserror::Error;

use crate::commands::*;

const DEFAULT_CHANNEL: &str = "main";
const PROTOCOL_VERSION: usize = 3;

#[derive(Clap, Debug)]
#[clap(
    version,
    author,
    global_setting(AppSettings::ColoredHelp),
    setting(AppSettings::InferSubcommands)
)]
pub struct Opts {
    #[clap(subcommand)]
    pub subcmd: SubCommand,
}

#[derive(Clap, Debug)]
pub enum SubCommand {
    /// Initializes an empty pijul repository
    Init(Init),

    /// Clones an existing pijul repository
    Clone(Clone),

    /// Creates a new change
    Record(Record),

    /// Shows difference between two channels/changes
    Diff(Diff),

    /// Show the entire log of changes
    Log(Log),

    /// Pushes changes to a remote upstream
    Push(Push),

    /// Pulls changes from a remote upstream
    Pull(Pull),

    /// Shows information about a particular change
    Change(Change),

    /// Manages different channels
    Channel(Channel),

    #[clap(setting = AppSettings::Hidden)]
    Protocol(Protocol),

    #[cfg(feature = "git")]
    /// Imports a git repository into pijul
    Git(Git),

    /// Moves a file in the working copy and the tree
    Mv(Mv),

    /// Lists files tracked by pijul
    Ls(Ls),

    /// Adds a path to the tree.
    ///
    /// Pijul has an internal tree to represent the files currently
    /// tracked. This command adds files and directories to that tree.
    Add(Add),

    /// Removes a file from the tree and prestine
    Remove(Remove),

    /// Resets the working copy to the last recorded change.
    ///
    /// In other words, discards all unrecorded changes.
    Reset(Reset),

    #[cfg(debug_assertions)]
    Debug(Debug),

    /// Create a new channel
    Fork(Fork),

    /// Unrecords all changes upto the given hash
    Unrecord(Unrecord),

    /// Applies changes to a channel
    Apply(Apply),

    /// Manages remote repositories
    Remote(Remote),

    /// Creates an archive of the repository
    Archive(Archive),

    /// Shows which patch last affected each line of the every file
    Credit(Credit),

    #[clap(external_subcommand)]
    ExternalSubcommand(Vec<OsString>),
}

#[derive(Debug, Error)]
pub enum Error {
    #[error("No Pijul repository found")]
    NoRepoRoot,
    #[error("Cannot access working directory")]
    CannotAccessWorkingDirectory,
    #[error("Already in a repository")]
    AlreadyInARepo,
    #[error("No such channel: {}", channel)]
    NoSuchChannel { channel: String },
    #[error("Protocol error. Is this the correct URL?")]
    ProtocolError { line: Vec<u8> },
    #[error("Not authenticated")]
    NotAuthenticated,
    #[error("No change message")]
    NoChangeMessage,
    #[error("Incorrect remote: {}", name)]
    IncorrectRemote { name: String },
    #[error("Unknown host key")]
    UnknownHostKey,
    #[error("Cannot record a binary change interactively. Use -a")]
    RecordBinaryChange,
    #[error("Remote not found: {:?}", remote)]
    RemoteNotFound { remote: String },
    #[error("No global config directory")]
    NoGlobalConfigDir,
    #[error("Could not parse global config")]
    CouldNotParseGlobal,
    #[error("Remote error: {}", msg)]
    Remote { msg: String },
    #[error("Remote exited with status {}", status)]
    RemoteExit { status: u32 },
    #[error("Missing remote")]
    MissingRemote,
    #[error("State not found in remote: {:?}", state)]
    StateNotFound { state: libpijul::pristine::Merkle },
    #[error("Missing dependencies for change {:?}", h)]
    MissingDep { h: libpijul::pristine::Hash },
    #[error("Ambiguous path: {:?}", path)]
    AmbiguousPath { path: String },
    #[error("No prefixes given. Use `.` to record the current directory.")]
    NoRecordPrefixes,
    #[error("HTTP error: {}", status.as_str())]
    Http { status: reqwest::StatusCode },
    #[error("Could not parse configuration file at {:?}", path)]
    CouldNotReadConfig { path: PathBuf },
    #[error("No current channel")]
    NoCurrentChannel,
    #[error("Cannot delete the current channel")]
    CannotDeleteCurrentChannel,
    #[error("Channel not found: {:?}", channel)]
    ChannelNotFound { channel: String },
    #[error("Cannot reset, because there are unrecorded changes")]
    UnrecordedChanges,
    #[error("Could not infer repository name")]
    CouldNotInferRepositoryName { repo: String },
    #[error("Changes not found: {:?}", hashes)]
    ChangesNotFound { hashes: Vec<String> },
    #[error("Cannot unrecord change {}, because change {} depends on it", change, dep.to_base32())]
    CannotUnrecord {
        change: String,
        dep: libpijul::pristine::Hash,
    },
}

#[tokio::main]
async fn main() {
    setup_panic!();
    env_logger::init();

    let opts = Opts::parse();

    if let Err(e) = run(opts).await {
        writeln!(std::io::stderr(), "Error: {}", e).unwrap_or(());
        std::process::exit(1);
    } else {
        std::process::exit(0);
    }
}

#[cfg(unix)]
fn run_external_command(mut command: Vec<OsString>) -> Result<(), std::io::Error> {
    let args = command.split_off(1);
    let mut cmd: OsString = "pijul-".into();
    cmd.push(&command[0]);

    use std::os::unix::process::CommandExt;
    writeln!(
        std::io::stderr(),
        "Error while running {:?}: {}",
        cmd,
        std::process::Command::new(&cmd).args(args).exec()
    )
    .unwrap_or(());
    std::process::exit(1)
}

#[cfg(windows)]
fn run_external_command(mut command: Vec<OsString>) -> Result<(), std::io::Error> {
    let args = command.split_off(1);
    let mut cmd: OsString = "pijul-".into();
    cmd.push(&command[0]);

    let mut spawned = match std::process::Command::new(&cmd).args(args).spawn() {
        Ok(spawned) => spawned,
        Err(e) => {
            writeln!(std::io::stderr(), "Error while running {:?}: {}", cmd, e).unwrap_or(());
            std::process::exit(1)
        }
    };
    let status = spawned.wait()?;
    std::process::exit(status.code().unwrap_or(1))
}

async fn run(opts: Opts) -> Result<(), anyhow::Error> {
    match opts.subcmd {
        SubCommand::Log(l) => l.run(),
        SubCommand::Init(init) => init.run(),
        SubCommand::Clone(clone) => clone.run().await,
        SubCommand::Record(record) => record.run().await,
        SubCommand::Diff(diff) => diff.run(),
        SubCommand::Push(push) => push.run().await,
        SubCommand::Pull(pull) => pull.run().await,
        SubCommand::Change(change) => change.run(),
        SubCommand::Channel(channel) => channel.run(),
        SubCommand::Protocol(protocol) => protocol.run(),
        #[cfg(feature = "git")]
        SubCommand::Git(git) => git.run(),
        SubCommand::Mv(mv) => mv.run(),
        SubCommand::Ls(ls) => ls.run(),
        SubCommand::Add(add) => add.run(),
        SubCommand::Remove(remove) => remove.run(),
        SubCommand::Reset(reset) => reset.run(),
        #[cfg(debug_assertions)]
        SubCommand::Debug(debug) => debug.run(),
        SubCommand::Fork(fork) => fork.run(),
        SubCommand::Unrecord(unrecord) => unrecord.run(),
        SubCommand::Apply(apply) => apply.run(),
        SubCommand::Remote(remote) => remote.run(),
        SubCommand::Archive(archive) => archive.run().await,
        SubCommand::Credit(credit) => credit.run(),
        SubCommand::ExternalSubcommand(command) => Ok(run_external_command(command)?),
    }
}

pub fn current_dir() -> Result<PathBuf, Error> {
    std::env::current_dir().map_err(|_| Error::CannotAccessWorkingDirectory)
}
