// Copyright 2023-2025 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2
#![deny(clippy::pedantic)]
#![deny(clippy::unwrap_used)]

use std::{
    fs::File,
    io::{Write, read_to_string},
    path::PathBuf,
    pin::Pin,
    process::ExitCode,
    sync::{
        Arc,
        atomic::{AtomicU32, Ordering},
    },
    time::Duration,
};

use anyhow::{Context, anyhow};
use camino::Utf8PathBuf;
use futures_util::StreamExt as _;
use futures_util::stream::{self, Stream};
use log::{debug, error, info, trace, warn};
use tokio::task::JoinSet;
use vstorage::sync::{
    Side,
    conflict::{KeepSideResolver, resolve_conflicts as apply_conflict_resolution},
    declare::StoragePair,
    execute::Executor,
    operation::Operation,
    plan::{Plan, PlanError},
    status::StatusDatabase,
};

use crate::{
    cli::{Cli, Command, FilterNames, ListTarget, print_usage},
    config::open_default_path,
    conflict::{Flow, YesNoQuit, continue_skip_or_quit, interactive_resolution},
    daemon::Daemon,
    error::Error,
    parseconf::{list_pair_names, list_storage_names, parse_config},
    repair::{NamedStorage, repair_storages},
};

mod cli;
mod config;
mod conflict;
mod daemon;
mod error;
mod parseconf;
mod proxy;
mod repair;
mod timeout;
mod tls;
mod transport;

/// Current app version (determined at compile-time).
pub const VERSION: &str = env!("PIMSYNC_VERSION");

/// Per-storage conflict resolution mechanism.
#[derive(PartialEq, Debug)]
pub(crate) enum ConflictResolution {
    /// In case of conflict, keep the contents of storage a.
    KeepA,
    /// In case of conflict, keep the contents of storage b.
    KeepB,
    /// In case of conflict, fix it running a command.
    Cmd(RawCommand),
}

/// Pair with a name, as defined in the configuration file.
pub(crate) struct NamedPair {
    name: String,
    pub(crate) inner: StoragePair,
    status_path: Utf8PathBuf,
    conflict_resolution: Option<ConflictResolution>,
    intervals: (Duration, Duration),
}

/// Data necessary to create a new `Command` instance.
///
/// Contrary to [`std::process::Command`], this can be used more than once.
#[derive(PartialEq, Debug)]
pub struct RawCommand {
    command: String,
    args: Vec<String>,
}

impl RawCommand {
    #[must_use]
    pub fn command(&self) -> std::process::Command {
        let mut cmd = std::process::Command::new(&self.command);
        cmd.args(&self.args);
        cmd
    }

    /// Create a new `RawCommand` from the given arguments.
    ///
    /// Returns `None` if arguments are empty.
    pub(crate) fn from_args<I>(mut args: I) -> Option<Self>
    where
        I: Iterator<Item = String>,
    {
        Some(RawCommand {
            command: args.next()?,
            args: args.collect(),
        })
    }
}

impl NamedPair {
    /// Apply conflict resolution to a plan stream if configured.
    fn apply_conflict_resolution_to_plan(
        &self,
        plan: impl Stream<Item = Result<Operation, PlanError>> + Send + 'static,
    ) -> Pin<Box<dyn Stream<Item = Result<Operation, PlanError>> + Send>> {
        match self.conflict_resolution {
            Some(ConflictResolution::KeepA) => {
                info!("Auto-resolving conflicts by keeping version from storage A");
                Box::pin(apply_conflict_resolution(plan, KeepSideResolver(Side::A)))
            }
            Some(ConflictResolution::KeepB) => {
                info!("Auto-resolving conflicts by keeping version from storage B");
                Box::pin(apply_conflict_resolution(plan, KeepSideResolver(Side::B)))
            }
            // TODO: RawCmd variant.
            _ => Box::pin(plan),
        }
    }

    /// Collect plan operations, print summary, and return them.
    ///
    /// Also returns the total amount of conflicts which require manual resolution.
    async fn print_and_collect_plan(
        &self,
        mut plan: Pin<Box<dyn Stream<Item = Result<Operation, PlanError>> + Send>>,
    ) -> (Vec<Result<Operation, PlanError>>, u32) {
        let mut operations = Vec::new();
        let mut collection_count = 0;
        let mut item_count = 0;
        let mut property_count = 0;
        let mut conflict_count = 0;
        let mut stale_count = 0;
        println!(">> Storage: {}", self.name);
        while let Some(result) = plan.next().await {
            match &result {
                Ok(operation) => match operation {
                    Operation::FlushStaleMappings { stale_uids } => {
                        debug!("stale uids: {stale_uids:?}");
                        stale_count += stale_uids.len();
                    }
                    Operation::Collection(collection_op) => {
                        debug!("collection: {collection_op:?}");
                        collection_count += 1;
                        println!("-> Collection {collection_op}");
                    }
                    Operation::Item(item_op) => {
                        debug!("item: {item_op:?}");
                        item_count += 1;
                        if operation.is_conflict() {
                            conflict_count += 1;
                        }
                        if let Some(uid) = item_op.uid() {
                            println!("-> Item {uid}: {item_op}");
                        } else {
                            println!("-> Item: {item_op}");
                        }
                    }
                    Operation::Property(prop_op) => {
                        debug!("property: {prop_op:?}");
                        property_count += 1;
                        if operation.is_conflict() {
                            conflict_count += 1;
                        }
                        let prop = prop_op.property;
                        println!("-> Property {prop}: {prop_op}");
                    }
                },
                Err(err) => {
                    error!("Error in plan: {err:?}");
                }
            }
            operations.push(result);
        }
        println!(
            "{collection_count} collection operations, {item_count} item operations, {property_count} property operations"
        );
        println!("{conflict_count} conflicts detected");
        println!("{stale_count} stale mappings to flush");
        println!();
        (operations, conflict_count)
    }

    /// Synchronise this pair once.
    ///
    /// Returns the number of unresolved conflicts in the plan.
    async fn sync_once(
        &self,
        dry_run: bool,
        interactive: bool,
        error_count: &Arc<AtomicU32>,
    ) -> Result<u32, Error> {
        let plan = self.create_plan().await?;
        let plan = self.apply_conflict_resolution_to_plan(plan);
        let (operations, conflicts) = self.print_and_collect_plan(plan).await;

        if interactive {
            match continue_skip_or_quit("Execute plan?").map_err(|e| Error::Other(e.into()))? {
                YesNoQuit::Yes => {}
                YesNoQuit::No => {
                    println!("Sync skipped for pair '{}'.", self.name);
                    return Ok(conflicts);
                }
                YesNoQuit::Quit => {
                    println!("Sync cancelled.");
                    return Err(Error::UserAbort);
                }
            }
        }

        if dry_run {
            return Ok(conflicts);
        }

        let status_rw = StatusDatabase::open_or_create(&self.status_path)
            .with_context(|| format!("open_or_create status db for {}", self.name))
            .map_err(Error::StatusFatal)?;
        let status_rw = Arc::new(status_rw);
        let executor = Executor::new(
            Arc::clone(self.inner.storage_a()),
            Arc::clone(self.inner.storage_b()),
            status_rw.clone(),
        );
        let mut operations_stream = stream::iter(operations);
        while let Some(next) = operations_stream.next().await {
            let op = match next {
                Ok(op) => op,
                Err(e) => {
                    error_count.fetch_add(1, Ordering::Relaxed);
                    error!("Planning error: {e:?}");
                    continue;
                }
            };

            if op.is_conflict() {
                continue;
            }

            match executor.execute_operation(op).await {
                Ok(Ok(())) => {}
                Ok(Err(e)) => {
                    error_count.fetch_add(1, Ordering::Relaxed);
                    error!("Execution error: {e:?}");
                }
                Err(status_err) => {
                    return Err(Error::StatusFatal(status_err.into()));
                }
            }
        }
        Ok(conflicts)
    }

    async fn create_plan(&self) -> Result<Plan, Error> {
        debug!("Creating plan for storage pair '{}'.", self.name);
        let status = StatusDatabase::open(&self.status_path)
            .with_context(|| format!("open status db for {}", self.name))
            .map_err(Error::StatusFatal)?;

        Ok(Plan::new(self.inner.clone(), status.map(Arc::new)).await?)
    }
}

#[tokio::main]
async fn main() -> ExitCode {
    match try_main().await {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("{e}");
            e.into()
        }
    }
}

async fn try_main() -> Result<(), Error> {
    let cli = Cli::parse(std::env::args()).map_err(|e| {
        print_usage();
        Error::Usage(e.into())
    })?;

    if let Command::Version = cli.command {
        println!("pimsync {VERSION}");
        return Ok(());
    }

    simple_logger::SimpleLogger::new()
        .with_level(cli.log_level)
        .init()
        .expect("logger should initialise");
    info!("Logging enabled with {} level", cli.log_level);

    let (config_path, config_file) = match cli.config_file {
        Some(file) => {
            let path = PathBuf::from(file);
            let file = File::open(&path)
                .with_context(|| format!("Could not open {}.", path.to_string_lossy()))
                .map_err(Error::Config)?;
            debug!("Opened config file {}", path.to_string_lossy());
            (path, file)
        }
        None => open_default_path().map_err(Error::Config)?,
    };
    let config_data = read_to_string(config_file).map_err(|e| Error::Config(e.into()))?;

    match cli.command {
        Command::Repair => {
            let config = parse_config(&config_data, FilterNames::new()).map_err(Error::Config)?;
            let storages = config.into_storages(cli.names).await?;
            info!("Parsed storages from config.");
            return repair_storages(storages).await.map_err(Error::Other);
        }
        Command::List { target } => {
            let names = match target {
                ListTarget::Pairs => list_pair_names(&config_data).map_err(Error::Config)?,
                ListTarget::Storages => list_storage_names(&config_data).map_err(Error::Config)?,
            };
            for name in names {
                println!("{name}");
            }
            return Ok(());
        }
        Command::Discover => {
            let config = parse_config(&config_data, FilterNames::new()).map_err(Error::Config)?;
            let storages = config.into_storages(cli.names).await?;
            for NamedStorage { name, storage } in storages {
                println!("\x1b[1m\x1b[32m==>\x1b[0m Discovering collections in {name}…");
                let disco = storage
                    .discover_collections()
                    .await
                    .map_err(|e| Error::Other(e.into()))?;
                for collection in disco.collections() {
                    println!("Found {}", collection.href());
                }
            }
            info!("Parsed storages from config.");
            return Ok(());
        }
        _ => (),
    }

    let config = parse_config(&config_data, cli.names)
        .with_context(|| format!("Could not parse file {}", config_path.display()))
        .map_err(Error::Config)?;
    trace!("Parsed configuration: {:?}", &config);
    let pairs = config.into_named_pairs().await?;

    match cli.command {
        Command::Check => Ok(()), // Implicitly checked pairs above.
        Command::Daemon { ready_fd } => {
            // Everything is ready; indicate this before actual daemon work.
            if let Some(mut f) = ready_fd {
                f.write_all(b"READY=1\n")
                    .context("writing to readiness fd")
                    .map_err(Error::Other)?;
                // File is closed implicitly here.
            }
            daemon(pairs).await
        }
        Command::Sync {
            dry_run,
            interactive,
        } => sync(pairs, dry_run, interactive).await,
        Command::ResolveConflicts { dry_run } => resolve_conflicts(pairs, dry_run).await,
        Command::Discover | Command::List { .. } | Command::Repair | Command::Version => {
            unreachable!("Handled above")
        }
    }
}

/// Returns only if all daemon tasks fail.
async fn daemon(pairs: Vec<NamedPair>) -> Result<(), Error> {
    warn!("Partial sync is not implemented; will perform full sync");

    let mut set = pairs
        .into_iter()
        .map(async |p| match Daemon::new(p).await {
            Ok(d) => d.run().await,
            Err(e) => e,
        })
        .collect::<JoinSet<_>>();

    let mut fatal: Option<Error> = None;
    while let Some(res) = set.join_next().await {
        let err = match res {
            Ok(e) => {
                error!("Error in daemon task: {e:?}.");
                e
            }
            Err(joinerr) => {
                error!("Daemon task aborted: {joinerr:?}.");
                Error::Other(joinerr.into())
            }
        };
        if matches!(err, Error::StatusFatal(_)) || fatal.is_none() {
            fatal = Some(err);
        }
    }

    if let Some(e) = fatal {
        Err(e)
    } else {
        Err(Error::Other(anyhow!("All sync tasks exited.")))
    }
}

/// Synchronise all pairs once.
async fn sync(pairs: Vec<NamedPair>, dry_run: bool, interactive: bool) -> Result<(), Error> {
    let error_count = Arc::new(AtomicU32::new(0));
    let mut total_conflicts = 0u32;

    // Run sequentially, so rendered plans don't get intermixed.
    for pair in pairs {
        match pair.sync_once(dry_run, interactive, &error_count).await {
            Ok(conflicts) => total_conflicts += conflicts, // Continue to next pair.
            Err(err) => {
                if matches!(err, Error::UserAbort) {
                    return Err(err);
                }
                error_count.fetch_add(1, Ordering::Relaxed);
                error!("Synchronising pair {}: {err}", pair.name);
            }
        }
    }

    if total_conflicts > 0 {
        warn!("{total_conflicts} conflicts require manual resolution.");
        return Err(Error::Conflict);
    }

    let total_errors = error_count.load(Ordering::Relaxed);
    if total_errors > 0 {
        return Err(Error::Other(anyhow!(
            "{total_errors} sync operations failed"
        )));
    }

    Ok(())
}

/// Interactively resolve conflicts.
///
/// Storages are resolved sequentially, since each item will require user intervention.
async fn resolve_conflicts(pairs: Vec<NamedPair>, dry_run: bool) -> Result<(), Error> {
    if dry_run {
        return Err(Error::Usage(anyhow!(
            "dry_run is not implemented for resolve-conflicts"
        )));
    }

    let mut result = Ok(());
    for pair in pairs {
        match interactive_resolution(pair).await {
            Ok(Flow::Continue) => {}
            Ok(Flow::Quit) => return Err(Error::UserAbort),
            Err(err) => {
                error!("Error resolving conflicts: {err:?}.");
                result = Err(err);
            }
        }
    }

    result
}
