// Copyright 2025 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

use std::{
    borrow::Cow,
    io::{Seek as _, Write as _, read_to_string, stdin, stdout},
    sync::Arc,
};

use anyhow::{Context as _, anyhow, bail};

use futures_util::StreamExt;
use futures_util::future::OptionFuture;
use log::{debug, error, info};
use rustix::fs::sync;
use tempfile::NamedTempFile;
use vstorage::{
    Etag,
    base::{Item, ItemVersion},
    property::Property,
    sync::{
        conflict::ConflictInfo,
        execute::Executor,
        mapping::ResolvedMapping,
        operation::{
            CollectionOp, ItemOp, MappingUidSource, Operation, PropertyOp, PropertyOpKind,
            StatusAction,
        },
        status::{MappingUid, StatusDatabase, StatusVersions},
    },
};

use crate::{ConflictResolution, NamedPair, RawCommand, error::Error};

pub enum Flow {
    Continue,
    Quit,
}

impl Flow {
    pub fn quit(&self) -> bool {
        match self {
            Flow::Continue => false,
            Flow::Quit => true,
        }
    }
}

/// Performs conflict resolution for this storage pair.
///
/// Hint: use the `testing/conflicts.sh` script to interactively test this.
pub async fn interactive_resolution(pair: NamedPair) -> Result<Flow, Error> {
    let raw_cmd = match pair.conflict_resolution {
        Some(ConflictResolution::Cmd(ref rc)) => rc,
        Some(_) => {
            info!("Conflict resolution is automatic for {}.", pair.name);
            return Ok(Flow::Continue);
        }
        _ => {
            return Err(Error::Usage(anyhow!(
                "No conflict resolution command for {}.",
                pair.name
            )));
        }
    };

    let status = StatusDatabase::open_or_create(&pair.status_path)?;
    let executor = Executor::new(
        Arc::clone(pair.inner.storage_a()),
        Arc::clone(pair.inner.storage_b()),
        Arc::new(status),
    );

    let mut plan = pair.create_plan().await?;

    // Collect in order to compute the total amount (for display purposes).
    let mut item_conflicts = Vec::new();
    let mut property_conflicts = Vec::new();
    while let Some(result) = plan.next().await {
        match result {
            Ok(op @ Operation::Collection(CollectionOp::SaveToStatus { .. })) => {
                // Ensure that collections have a mapping_uid.
                match executor.execute_operation(op).await {
                    Ok(Ok(())) => {}
                    Ok(Err(sync_err)) => return Err(Error::Other(sync_err.into())),
                    Err(status_err) => return Err(Error::StatusFatal(status_err.into())),
                }
            }
            Ok(Operation::Item(ItemOp::Conflict {
                info, mapping_uid, ..
            })) => {
                let mapping_uid = mapping_uid
                    .resolve()
                    .await
                    .context("resolving mapping uid for item conflict")
                    .map_err(Error::StatusFatal)?;
                item_conflicts.push((info, mapping_uid));
            }
            Ok(Operation::Property(
                op @ PropertyOp {
                    kind: PropertyOpKind::Conflict { .. },
                    ..
                },
            )) => {
                let mapping_uid = op
                    .mapping_uid
                    .clone()
                    .resolve()
                    .await
                    .context("resolving mapping uid for property conflict")
                    .map_err(Error::StatusFatal)?;
                property_conflicts.push((op, mapping_uid));
            }
            Ok(_) => {} // Skip non-conflict operations
            Err(err) => {
                error!("Error in plan: {err:?}");
            }
        }
    }

    let item_total = item_conflicts.len();
    let property_total = property_conflicts.len();
    if item_total == 0 && property_total == 0 {
        info!("No conflicts to resolve for {}.", pair.name);
        return Ok(Flow::Continue);
    }
    println!(
        "Resolving {} item conflicts and {} property conflicts for pair \"{}\".",
        item_total, property_total, pair.name
    );

    if resolve_item_conflicts(&pair, &executor, item_conflicts, raw_cmd)
        .await
        .map_err(Error::Other)?
        .quit()
    {
        return Ok(Flow::Quit);
    }
    if resolve_property_conflicts(&pair, &executor, property_conflicts, raw_cmd)
        .await
        .map_err(Error::Other)?
        .quit()
    {
        return Ok(Flow::Quit);
    }

    println!("No conflicts left for pair \"{}\".", pair.name);

    Ok(Flow::Continue)
}

/// Resolve item conflicts interactively. Returns `false` if user chose to quit.
async fn resolve_item_conflicts(
    pair: &NamedPair,
    executor: &Executor,
    conflicts: Vec<(ConflictInfo, MappingUid)>,
    raw_cmd: &RawCommand,
) -> anyhow::Result<Flow> {
    let total = conflicts.len();
    for (i, (info, mapping_uid)) in conflicts.into_iter().enumerate() {
        println!(
            "Next is item {}/{total}, with uid \"{}\".",
            i + 1,
            info.a.state.uid
        );
        match continue_skip_or_quit("Resolve it manually?")? {
            YesNoQuit::Yes => {}
            YesNoQuit::No => continue,
            YesNoQuit::Quit => {
                println!("Quitting conflict resolution for pair \"{}\".", pair.name);
                return Ok(Flow::Quit);
            }
        }
        info!("Running conflict resolution for item {}", info.a.state.uid);

        let temp_a = write_to_temp_file(info.a.data.as_str().as_bytes())
            .context("writing item A to temp file")?;
        let temp_b = write_to_temp_file(info.b.data.as_str().as_bytes())
            .context("writing item B to temp file")?;

        let new = match run_merge_command(raw_cmd, temp_a, temp_b) {
            Ok(data) => Item::from(data),
            Err(err) => {
                error!("Error resolving conflict: {err}");
                continue;
            }
        };

        let (etag_a, etag_b) = match upload_resolved_item(pair, &info, &new).await {
            Ok(versions) => versions,
            Err(err) => {
                error!("Error uploading resolved item: {err}");
                continue;
            }
        };

        let uid = new.ident();
        info!("Resolved conflict for '{uid}', updating status.");

        let ConflictInfo { a, b, old, .. } = info;
        let versions = StatusVersions {
            a: ItemVersion {
                href: a.state.version.href,
                etag: etag_a,
            },
            b: ItemVersion {
                href: b.state.version.href,
                etag: etag_b,
            },
        };
        let action = match old {
            Some(old) => StatusAction::Update {
                hash: new.hash(),
                old,
                new: versions,
            },
            None => StatusAction::Insert {
                uid,
                hash: new.hash(),
                versions,
            },
        };
        executor.execute_item_status_action(action, mapping_uid)?;

        debug!("Status updated.");
    }
    Ok(Flow::Continue)
}

/// Resolve property conflicts interactively.
async fn resolve_property_conflicts(
    pair: &NamedPair,
    executor: &Executor,
    conflicts: Vec<(PropertyOp, MappingUid)>,
    raw_cmd: &RawCommand,
) -> anyhow::Result<Flow> {
    let total = conflicts.len();
    for (i, (op, mapping_uid)) in conflicts.into_iter().enumerate() {
        let PropertyOpKind::Conflict { value_a, value_b } = op.kind else {
            unreachable!()
        };
        let property = op.property;
        let mapping = op.mapping;

        println!(
            "Property {}/{total}: \"{}\" on collection \"{}\":",
            i + 1,
            property.name(),
            mapping.alias()
        );
        println!("  A: \"{value_a}\"");
        println!("  B: \"{value_b}\"");

        let resolved = match choose_property_value(&value_a, &value_b, raw_cmd)? {
            PropertyChoice::A => Cow::Borrowed(value_a.as_str()),
            PropertyChoice::B => Cow::Borrowed(value_b.as_str()),
            PropertyChoice::Custom(value) => value,
            PropertyChoice::Skip => continue,
            PropertyChoice::Quit => {
                println!(
                    "Quitting property conflict resolution for pair \"{}\".",
                    pair.name
                );
                return Ok(Flow::Quit);
            }
        };

        info!("Resolved property '{}' to '{}'", property.name(), resolved);

        if let Err(err) =
            upload_resolved_property(pair, &property, &mapping, &value_a, &value_b, &resolved).await
        {
            error!("Error setting resolved property: {err}");
            continue;
        }

        let op = Operation::Property(PropertyOp {
            property,
            mapping,
            mapping_uid: MappingUidSource::Immediate(mapping_uid),
            on_complete: None,
            kind: PropertyOpKind::UpdateStatus {
                value: resolved.into_owned(),
            },
        });
        executor.execute_operation(op).await??;
    }
    Ok(Flow::Continue)
}

pub enum YesNoQuit {
    Yes,
    No,
    Quit,
}

enum PropertyChoice<'a> {
    A,
    B,
    Custom(Cow<'a, str>),
    Skip,
    Quit,
}

pub fn continue_skip_or_quit(msg: &str) -> std::io::Result<YesNoQuit> {
    loop {
        print!("{msg} (Y)es, (N)o, or (Q)uit? ");
        stdout().flush()?;
        // Need to read entire lines because the stdlib implicitly buffers stdin.
        let mut response = String::new();
        stdin().read_line(&mut response)?;

        match response.trim().to_lowercase().as_str() {
            "" | "y" => return Ok(YesNoQuit::Yes),
            "n" => return Ok(YesNoQuit::No),
            "q" => return Ok(YesNoQuit::Quit),
            _ => {}
        }
    }
}

/// Prompt user to choose a property value: keep A, keep B, edit, skip, or quit.
fn choose_property_value<'a>(
    value_a: &str,
    value_b: &str,
    raw_cmd: &RawCommand,
) -> anyhow::Result<PropertyChoice<'a>> {
    loop {
        print!("Keep (A), (B), (E)dit, (S)kip, or (Q)uit? ");
        stdout().flush()?;

        let mut response = String::new();
        stdin()
            .read_line(&mut response)
            .context("Reading response from stdin")?;

        match response.trim().to_lowercase().as_str() {
            "a" => return Ok(PropertyChoice::A),
            "b" => return Ok(PropertyChoice::B),
            "e" => {
                let temp_a = write_to_temp_file(value_a.as_bytes())?;
                let temp_b = write_to_temp_file(value_b.as_bytes())?;
                match run_merge_command(raw_cmd, temp_a, temp_b).map(|s| s.trim().to_string()) {
                    Ok(value) => return Ok(PropertyChoice::Custom(Cow::Owned(value))),
                    Err(err) => {
                        error!("Error editing property: {err}");
                        // Let user try again
                    }
                }
            }
            "s" => return Ok(PropertyChoice::Skip),
            "q" => return Ok(PropertyChoice::Quit),
            _ => {}
        }
    }
}

/// Run a merge command on two temporary files and validate the result.
///
/// Execute the command with both file paths as arguments. After the command exits,
/// read both files back and validate that they are non-empty and identical.
fn run_merge_command(
    raw_cmd: &RawCommand,
    mut temp_a: NamedTempFile,
    mut temp_b: NamedTempFile,
) -> anyhow::Result<String> {
    let exit_status = raw_cmd
        .command()
        .arg(temp_a.path())
        .arg(temp_b.path())
        .spawn()
        .context("executing conflict resolution command")?
        .wait()
        .context("waiting for conflict resolution command")?;

    if !exit_status.success() {
        bail!("Conflict resolution command failed: {exit_status}");
    }

    // Ensure that files are committed to disk; otherwise we sometimes read empty data.
    sync();
    temp_a.rewind().context("seeking in temporary file for A")?;
    temp_b.rewind().context("seeking in temporary file for B")?;

    let new_a = read_to_string(temp_a).context("reading resolved value A")?;
    let new_b = read_to_string(temp_b).context("reading resolved value B")?;

    if new_a.trim().is_empty() {
        bail!("Resolved value A is empty.");
    }
    if new_b.trim().is_empty() {
        bail!("Resolved value B is empty.");
    }
    if new_a.trim() != new_b.trim() {
        println!("Resulting values are not identical on both sides.");
        bail!("Conflict resolution yielded mismatching values.");
    }
    Ok(new_a)
}

/// Write data to a new temporary file.
fn write_to_temp_file(data: &[u8]) -> anyhow::Result<NamedTempFile> {
    let mut temp = NamedTempFile::new().context("Creating temporary file")?;
    temp.write_all(data)
        .context("writing into temporary file")?;
    Ok(temp)
}

/// Upload a resolved item to both storages, returning the new versions for each side.
async fn upload_resolved_item(
    pair: &NamedPair,
    info: &ConflictInfo,
    new: &Item,
) -> anyhow::Result<(Etag, Etag)> {
    let ref_a = &info.a.state.version;
    let ref_b = &info.b.state.version;
    let orig_a = &info.a.data;
    let orig_b = &info.b.data;

    let task_a: OptionFuture<_> = (new.hash() != orig_a.hash())
        .then_some(async {
            pair.inner
                .storage_a()
                .update_item(&ref_a.href, &ref_a.etag, new)
                .await
                .context("uploading resolved item into A")
        })
        .into();
    let task_b: OptionFuture<_> = (new.hash() != orig_b.hash())
        .then_some(async {
            pair.inner
                .storage_b()
                .update_item(&ref_b.href, &ref_b.etag, new)
                .await
                .context("uploading resolved item into B")
        })
        .into();
    let (result_a, result_b) = tokio::join!(task_a, task_b);

    Ok((
        result_a.transpose()?.unwrap_or_else(|| ref_a.etag.clone()),
        result_b.transpose()?.unwrap_or_else(|| ref_b.etag.clone()),
    ))
}

/// Upload a resolved property to both storages.
async fn upload_resolved_property(
    pair: &NamedPair,
    property: &Property,
    mapping: &ResolvedMapping,
    orig_a: &str,
    orig_b: &str,
    new_value: &str,
) -> anyhow::Result<()> {
    let task_a: OptionFuture<_> = (new_value != orig_a)
        .then_some(async {
            pair.inner
                .storage_a()
                .set_property(mapping.a().href(), *property, new_value)
                .await
                .context("setting resolved property on A")
        })
        .into();
    let task_b: OptionFuture<_> = (new_value != orig_b)
        .then_some(async {
            pair.inner
                .storage_b()
                .set_property(mapping.b().href(), *property, new_value)
                .await
                .context("setting resolved property on B")
        })
        .into();
    let (result_a, result_b) = tokio::join!(task_a, task_b);
    result_a.transpose()?;
    result_b.transpose()?;
    Ok(())
}
