use bstr::ByteSlice;
use patchpal::common::git;
use patchpal::ui::{frontend::Frontend, frontend::ListChange, workspace::Workspace};
use sha1::{Digest, Sha1};
use std::{ffi::OsString, future::Future, rc::Rc};
use std::cell::{Cell, OnceCell, RefCell};

#[derive(Clone, derive_more::Deref)]
pub(crate) struct App(Rc<AppInner>);

pub(crate) struct AppInner {
    executor: smol::LocalExecutor<'static>,
    termination: smol::lock::Semaphore,
    exit_value: OnceCell<Result<u8, String>>,
    workspace: OnceCell<Workspace<App>>,
    processing: Cell<bool>,
    missing: RefCell<Vec<Rc<git::Description>>>,
    force: bool,
    do_missing: bool,
    do_telemetry: bool,
}

impl App {
    pub(crate) fn new(force: bool, do_missing: bool, do_telemetry: bool) -> Self {
        Self(Rc::new(AppInner {
            executor: smol::LocalExecutor::new(),
            termination: smol::lock::Semaphore::new(0),
            exit_value: OnceCell::new(),
            workspace: OnceCell::new(),
            processing: Cell::new(false),
            missing: RefCell::new(Vec::new()),
            force,
            do_missing,
            do_telemetry,
        }))
    }

    pub(crate) fn run(&self, workspace: Workspace<Self>) -> Result<u8, String> {
        workspace.telemetry().tele_set("frontend", "cli");
        workspace.telemetry().tele_set("cli_arg_force", self.force);
        workspace.telemetry().tele_set("cli_arg_missing", self.do_missing);
        self.workspace.set(workspace).unwrap_or_else(|_| panic!("workspace already set"));
        self.workspace.get().unwrap().start()?;
        smol::block_on(self.executor.run(self.termination.acquire()));
        let exit_value = self.exit_value.get().unwrap();
        if self.do_missing && exit_value.is_ok() {
            for desc in self.missing.borrow().iter() {
                println!("{} {}", desc.id[..14].as_bstr(), desc.subject.as_bstr());
            }
        }
        exit_value.clone()
    }

    fn step(&self) -> Result<(), String> {
        let workspace = self.workspace.get().unwrap();
        let mut pending = Ok(false);
        let mut pending_suggestions = false;
        workspace.for_each_block(|block| {
            match block.is_pending() {
                Ok(true) => if let Ok(val) = pending.as_mut() {
                    *val = true;
                }
                Ok(false) => {}
                Err(msg) => pending = Err(msg),
            }
            pending_suggestions |= block.has_pending_suggestions();
        });
        if pending? {
            return Ok(());
        }
        if self.do_missing {
            if pending_suggestions {
                return Ok(());
            }
            self.add_missing();
        }
        if workspace.apply_next()? {
            return Ok(());
        }
        if self.do_missing {
            workspace.dont_save();
            Ok(())
        } else {
            workspace.save()
        }
    }

    fn check_pending(&self) {
        if self.processing.get() || self.exit_value.get().is_some() {
            // we're processing a commit or exiting; don't do anything
            return;
        }
        if let Err(msg) = self.step() {
            self.exit_value.set(Err(msg)).unwrap();
            self.workspace.get().unwrap().abort();
        }
    }

    fn add_missing(&self) {
        let workspace = self.workspace.get().unwrap();
        let mut missing = self.missing.borrow_mut();
        workspace.for_each_block(|block| {
            if let Some(change) = block.try_as_change() {
                for desc in change.missing.iter().rev() {
                    // Check if this is not a duplicate. The number of missing commits is
                    // supposed to be small. Just use linear search.
                    if missing.iter().find(|m| m.id == desc.id).is_none() {
                        missing.push(desc.clone());
                    }
                }
            }
        });
    }
}

impl Frontend for App {
    fn spawn_future(&self, future: impl Future<Output = ()> + 'static) {
        self.executor.spawn(future).detach()
    }

    fn terminate(&self, exit_code: u8) {
        // ignore the exit_code if we previously set an error message
        let _ = self.exit_value.set(Ok(exit_code));
        self.termination.add_permits(1);
    }

    fn processing_commit(&self) {
        self.processing.set(true);
    }

    fn commit_applied(&self, _commit_index: usize) {
        self.processing.set(false);
        let workspace = self.workspace.get().unwrap();
        if !workspace.is_clean_cherry_pick() && !self.force && !self.do_missing {
            let _ = self.exit_value.set(Err(concat!(
                "This is not a clean cherry-pick and needs review. ",
                "As a precaution, the commit was not applied. ",
                "Re-run with --force to apply it anyway. Always review the result."
            ).to_string()));
            workspace.abort();
            return;
        }
        self.check_pending();
    }

    fn commit_failed(&self, msg: String, _commit_index: Option<usize>) {
        let _ = self.exit_value.set(Err(msg));
        self.workspace.get().unwrap().abort();
    }

    fn file_changed(&self, _file_index: usize) {
        self.check_pending();
    }

    fn view_changed(&self, _file_index: usize, _change: ListChange) {
        if self.do_missing {
            // Suggestions updates invoke view_changed. We can ignore it if we're not
            // interested in missing dependency commits.
            self.check_pending();
        }
    }

    async fn telemetry_agreement(&self) -> Option<uuid::Uuid> {
        if self.do_telemetry {
            let Ok(id) = machine_uid::get() else {
                #[cfg(feature = "debug_telemetry")]
                println!("telemetry: can't get machine id");
                return None;
            };
            let mut id = hex::decode(&id).unwrap_or(id.into());
            id.splice(0 .. 0, b"patchpal-cli:".iter().copied());
            let digest = Sha1::digest(id);
            Some(
                uuid::Builder::from_sha1_bytes(digest[..16].try_into().unwrap())
                    .into_uuid()
            )
        } else {
            None
        }
    }

    fn commit_list_changed(&self, _change: ListChange, _selected: Option<usize>) {}
    fn run_editor(
        &self, _path: OsString, _data: Vec<u8>, _done: Box<dyn FnOnce(Option<Vec<u8>>)>,
    ) {}
}
