//! Revisionsgebundene Exportaufträge und abwerfbare Hintergrundresultate.
use crate::documents::{Destination, DocumentId, Project};
use anyhow::{ensure, Result};
use std::path::PathBuf;
use tb_vm::project_io::{has_extension, Manifest};

pub const UNAVAILABLE: &str = "Native Erzeugung folgt in Phase 6";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Artifact {
    Executable,
    Library,
}
impl Artifact {
    pub fn label(self) -> &'static str {
        match self {
            Self::Executable => "Natives Standalone-Executable",
            Self::Library => "Portable P-Code-Bibliothek (.tbl)",
        }
    }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectStamp {
    pub path: Option<PathBuf>,
    pub manifest: Manifest,
    pub inputs: Vec<(PathBuf, Option<Vec<u8>>)>,
    pub include_paths: Vec<PathBuf>,
    pub revisions: Vec<(DocumentId, u64)>,
}
impl ProjectStamp {
    pub fn capture(project: &Project) -> Self {
        Self {
            path: project.path().map(PathBuf::from),
            manifest: project.manifest().clone(),
            include_paths: project.include_paths.clone(),
            inputs: project
                .sources()
                .map(|s| {
                    s.protected_inputs()
                        .into_iter()
                        .filter(|p| has_extension(p, "tbl") || project.find_document(p).is_none())
                        .map(|p| {
                            let b = std::fs::read(&p).ok();
                            (p, b)
                        })
                        .collect()
                })
                .unwrap_or_default(),
            revisions: project
                .documents()
                .map(|(id, d)| (id, d.revision()))
                .collect(),
        }
    }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportRequest {
    pub generation: u64,
    pub project: ProjectStamp,
    pub artifact: Artifact,
    pub system: String,
    pub architecture: String,
    pub path: PathBuf,
    pub overwrite: bool,
    pub previous_output: Option<Vec<u8>>,
}
impl ExportRequest {
    pub fn validate(
        project: &Project,
        artifact: Artifact,
        system: &str,
        architecture: &str,
        destination: &Destination,
    ) -> Result<Self> {
        if artifact == Artifact::Executable {
            target(system, architecture)?;
        }
        ensure!(
            !has_extension(&destination.path, "tbc"),
            "TBC ist kein natives Exportartefakt"
        );
        let path = project.validate_export_target(destination)?;
        if artifact == Artifact::Library {
            ensure!(has_extension(&path, "tbl"), "Library-Ausgabe benötigt .tbl");
        }
        static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
        Ok(Self {
            generation: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
            project: ProjectStamp::capture(project),
            artifact,
            system: system.into(),
            architecture: architecture.into(),
            previous_output: std::fs::read(&path).ok(),
            path,
            overwrite: destination.overwrite,
        })
    }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExportStatus {
    Ready,
    Running,
    Success,
    Failed(String),
    Cancelled,
}
impl ExportStatus {
    pub fn text(&self) -> String {
        match self {
            Self::Ready => "Geprüft".into(),
            Self::Running => "Erzeugung läuft".into(),
            Self::Success => "Erfolgreich erzeugt".into(),
            Self::Failed(e) => format!("Erzeugung fehlgeschlagen: {e}"),
            Self::Cancelled => "Erzeugung abgebrochen".into(),
        }
    }
}

/// Die unterstützten Kombinationen werden ausschließlich aus dem Exportkatalog gewählt.
pub fn target(system: &str, architecture: &str) -> Result<tb_export::Target> {
    tb_export::Target::ALL
        .into_iter()
        .find(|t| target_parts(*t) == (system, architecture))
        .ok_or_else(|| anyhow::anyhow!("Unzulässiges EXE-Ziel: {system}/{architecture}"))
}
pub fn target_parts(target: tb_export::Target) -> (&'static str, &'static str) {
    let triple = target.triple();
    (
        if triple.contains("windows") {
            "windows"
        } else if triple.contains("apple") {
            "macos"
        } else {
            "linux"
        },
        triple.split('-').next().unwrap(),
    )
}

pub struct Job {
    pub request: ExportRequest,
    thread: Option<std::thread::JoinHandle<()>>,
    cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
    result: std::sync::mpsc::Receiver<Result<tb_export::PreparedPublication>>,
}
impl Drop for Job {
    fn drop(&mut self) {
        self.cancelled
            .store(true, std::sync::atomic::Ordering::Release);
        // Auch beim Beenden der IDE müssen kontrollierte Tools und Staging enden.
        if let Some(thread) = self.thread.take() {
            let _ = thread.join();
        }
    }
}
impl Job {
    pub fn start(
        request: ExportRequest,
        project: &Project,
        runtime_dir: &std::path::Path,
    ) -> Result<Self> {
        let sources = project.sources()?;
        let mut compiler = tb_vm::project::ProjectCompiler::default();
        compiler.debug_symbols = true;
        let diagnostics = |errors: Vec<tb_frontend::Diagnostic>| {
            anyhow::anyhow!(errors
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("\n"))
        };
        let bytes = match request.artifact {
            Artifact::Library => sources
                .compile_library(&mut compiler)
                .map_err(diagnostics)?
                .to_tbl()
                .map_err(|e| anyhow::anyhow!(e))?,
            Artifact::Executable => sources
                .compile(&mut compiler, "IDE")
                .map_err(diagnostics)?
                .to_tbc(),
        };
        ensure!(
            request.project == ProjectStamp::capture(project),
            "Projekt während der Übersetzung geändert"
        );
        let mut protected = sources.protected_inputs();
        protected.extend(
            project
                .documents()
                .map(|(_, d)| d.source_path().to_path_buf()),
        );
        if let Some(path) = project.path() {
            protected.push(path.into());
        }
        let runtime_dir = runtime_dir.to_path_buf();
        let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let flag = cancelled.clone();
        let (sender, result) = std::sync::mpsc::channel();
        let work = request.clone();
        let thread = std::thread::Builder::new()
            .name("tb-export".into())
            .spawn(move || {
                let cancelled = || flag.load(std::sync::atomic::Ordering::Acquire);
                let prepare = || -> Result<tb_export::PreparedPublication> {
                    if work.artifact == Artifact::Library {
                        tb_export::prepare_publication(
                            &work.path,
                            work.overwrite,
                            &protected,
                            &cancelled,
                            |p| {
                                std::fs::write(p, &bytes)?;
                                Ok(())
                            },
                        )
                    } else {
                        let target = target(&work.system, &work.architecture)?;
                        let template = runtime_dir
                            .join(target.triple())
                            .join(target.runtime_name());
                        let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes)
                            .map_err(|e| anyhow::anyhow!("{e}"))?;
                        tb_export::prepare_export(
                            tb_export::Export {
                                module: &module,
                                target,
                                template: &template,
                                output: &work.path,
                                overwrite: work.overwrite,
                                protected: &protected,
                            },
                            &cancelled,
                        )
                    }
                };
                // Ein geschlossener Empfänger verwirft das Staging-Ergebnis per Drop.
                let _ = sender.send(prepare());
            })?;
        Ok(Self {
            thread: Some(thread),
            request,
            cancelled,
            result,
        })
    }
    pub fn take_result(&self) -> Option<Result<tb_export::PreparedPublication>> {
        match self.result.try_recv() {
            Ok(result) => Some(result),
            Err(std::sync::mpsc::TryRecvError::Empty) => None,
            Err(_) => Some(Err(anyhow::anyhow!("Exportworker beendet ohne Ergebnis"))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{app::App, commands::Command};
    use std::{
        fs,
        sync::{
            atomic::{AtomicBool, AtomicU64, Ordering},
            mpsc, Arc,
        },
    };

    struct Dir(PathBuf);
    impl Dir {
        fn new() -> Self {
            static N: AtomicU64 = AtomicU64::new(0);
            let p = std::env::temp_dir().join(format!(
                "tb-export-ui-{}-{}",
                std::process::id(),
                N.fetch_add(1, Ordering::Relaxed)
            ));
            fs::create_dir_all(&p).unwrap();
            Self(p.canonicalize().unwrap())
        }
    }
    impl Drop for Dir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.0);
        }
    }
    fn delayed(
        app: &mut App,
        output: &std::path::Path,
    ) -> (
        mpsc::Sender<Result<tb_export::PreparedPublication>>,
        Arc<AtomicBool>,
    ) {
        app.execute(Command::MakeLibrary);
        let request = ExportRequest::validate(
            &app.project,
            Artifact::Library,
            "",
            "",
            &Destination {
                path: output.into(),
                overwrite: true,
            },
        )
        .unwrap();
        let d = app.dialog.as_mut().unwrap();
        d.request = Some(request.clone());
        d.export_status = ExportStatus::Running;
        let (tx, rx) = mpsc::channel();
        let cancelled = Arc::new(AtomicBool::new(false));
        app.export_job = Some(Job {
            thread: None,
            request,
            cancelled: cancelled.clone(),
            result: rx,
        });
        (tx, cancelled)
    }
    fn prepared(output: &std::path::Path) -> tb_export::PreparedPublication {
        tb_export::prepare_publication(output, true, &[], &|| false, |p| {
            fs::write(p, b"new")?;
            Ok(())
        })
        .unwrap()
    }
    fn no_staging(dir: &std::path::Path) {
        assert!(fs::read_dir(dir).unwrap().all(|p| !p
            .unwrap()
            .file_name()
            .to_string_lossy()
            .starts_with(".tb-export-")));
    }
    #[test]
    fn delayed_result_is_published_only_after_ui_validation_and_success_stays_success() {
        let dir = Dir::new();
        let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
        let output = dir.0.join("out.tbl");
        fs::write(&output, b"old").unwrap();
        let (tx, _) = delayed(&mut app, &output);
        let staged = prepared(&output);
        app.tick_export();
        app.handle(crossterm::event::Event::Resize(110, 35));
        assert_eq!(app.size, (110, 35));
        assert_eq!(fs::read(&output).unwrap(), b"old");
        tx.send(Ok(staged)).ok().unwrap();
        app.tick_export();
        assert_eq!(fs::read(&output).unwrap(), b"new");
        assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
        app.handle(crossterm::event::Event::Key(
            crossterm::event::KeyEvent::new(
                crossterm::event::KeyCode::Esc,
                crossterm::event::KeyModifiers::NONE,
            ),
        ));
        assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
        no_staging(&dir.0);
    }
    #[test]
    fn cancelled_superseded_changed_and_foreign_jobs_never_publish() {
        for mode in 0..5 {
            let dir = Dir::new();
            let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
            let output = dir.0.join("out.tbl");
            fs::write(&output, b"old").unwrap();
            let (tx, flag) = delayed(&mut app, &output);
            let staged = prepared(&output);
            match mode {
                0 => app.handle(crossterm::event::Event::Key(
                    crossterm::event::KeyEvent::new(
                        crossterm::event::KeyCode::Esc,
                        crossterm::event::KeyModifiers::NONE,
                    ),
                )),
                1 => {
                    let id = app.project.members()[0];
                    app.project.replace_text(id, 0..0, "PRINT 2\n").unwrap();
                }
                2 => {
                    app.project = Project::new(&dir.0).unwrap();
                }
                3 => {
                    let old = app.export_job.as_ref().unwrap().request.generation;
                    let (_new, _) = delayed(&mut app, &output);
                    assert_ne!(old, app.export_job.as_ref().unwrap().request.generation);
                }
                _ => {
                    fs::write(&output, b"external").unwrap();
                }
            }
            let _ = tx.send(Ok(staged));
            app.tick_export();
            assert!(flag.load(Ordering::Acquire));
            assert_eq!(
                fs::read(&output).unwrap(),
                if mode == 4 {
                    b"external".as_slice()
                } else {
                    b"old".as_slice()
                }
            );
            assert!(!app
                .last_export
                .as_ref()
                .is_some_and(|(_, s)| *s == ExportStatus::Success));
            no_staging(&dir.0);
        }
    }
    #[test]
    fn replaced_library_invalidates_pending_export_even_with_same_length() {
        let dir = Dir::new();
        let source = dir.0.join("lib.bas");
        fs::write(&source, "SUB Answer\nPRINT 1\nEND SUB\n").unwrap();
        let mut compiler = tb_vm::project::ProjectCompiler::default();
        let build = |compiler: &mut tb_vm::project::ProjectCompiler| {
            tb_vm::project_io::SourceLoader::default()
                .load(&source)
                .unwrap()
                .compile_library(compiler)
                .unwrap()
                .to_tbl()
                .unwrap()
        };
        let a = build(&mut compiler);
        fs::write(&source, "SUB Answer\nPRINT 2\nEND SUB\n").unwrap();
        let b = build(&mut compiler);
        assert_eq!(a.len(), b.len());
        let lib = dir.0.join("lib.tbl");
        fs::write(&lib, &a).unwrap();
        fs::write(dir.0.join("main.bas"), "CALL Answer\nEND\n").unwrap();
        fs::write(dir.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
        let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
        app.load_initial_project(dir.0.join("main.mak")).unwrap();
        let before = app.compile_current().unwrap().to_tbc();
        assert!(app.current_compilation().is_some());
        let out = dir.0.join("out.tbl");
        fs::write(&out, b"old").unwrap();
        let (tx, flag) = delayed(&mut app, &out);
        let staged = prepared(&out);
        fs::write(&lib, &b).unwrap();
        assert!(app.current_compilation().is_none());
        let _ = tx.send(Ok(staged));
        app.tick_export();
        assert!(flag.load(Ordering::Acquire));
        assert_eq!(fs::read(&out).unwrap(), b"old");
        assert_ne!(before, app.compile_current().unwrap().to_tbc());
        no_staging(&dir.0);
    }
}
