//! Geprüfte Runtime-Vorlagen, Nutzlastcontainer und geschützte Veröffentlichung.
use anyhow::{bail, ensure, Context, Result};
use std::{
    fs,
    path::{Path, PathBuf},
    sync::atomic::{AtomicU64, Ordering},
};
use tb_vm::bytecode::{CompiledModule, TBC_VERSION};

pub use tb_vm::bytecode::RUNTIME_VERSION;
const CONTAINER_VERSION: u32 = 1;
const MAGIC: &[u8; 8] = b"TBPCODE!";
const FOOTER: usize = 48;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Target {
    WindowsAmd64 = 1,
    MacosArm64 = 2,
    LinuxAmd64 = 3,
    LinuxArm64 = 4,
}
impl Target {
    pub const ALL: [Self; 4] = [
        Self::WindowsAmd64,
        Self::MacosArm64,
        Self::LinuxAmd64,
        Self::LinuxArm64,
    ];
    pub fn triple(self) -> &'static str {
        match self {
            Self::WindowsAmd64 => "x86_64-pc-windows-msvc",
            Self::MacosArm64 => "aarch64-apple-darwin",
            Self::LinuxAmd64 => "x86_64-unknown-linux-gnu",
            Self::LinuxArm64 => "aarch64-unknown-linux-gnu",
        }
    }
    pub fn parse(s: &str) -> Result<Self> {
        Self::ALL
            .into_iter()
            .find(|t| t.triple() == s)
            .with_context(|| format!("Nicht unterstütztes Target: {s}"))
    }
    pub fn host() -> Result<Self> {
        match (std::env::consts::OS, std::env::consts::ARCH) {
            ("windows", "x86_64") => Ok(Self::WindowsAmd64),
            ("macos", "aarch64") => Ok(Self::MacosArm64),
            ("linux", "x86_64") => Ok(Self::LinuxAmd64),
            ("linux", "aarch64") => Ok(Self::LinuxArm64),
            other => bail!("Kein unterstütztes Host-Target: {other:?}"),
        }
    }
    pub fn runtime_name(self) -> &'static str {
        if self == Self::WindowsAmd64 {
            "tbrt.exe"
        } else {
            "tbrt"
        }
    }
}
fn part(b: &[u8], at: usize, len: usize) -> Result<&[u8]> {
    b.get(at..at.checked_add(len).context("Längenüberlauf")?)
        .context("Abgeschnittenes natives Format/Nutzlast")
}
fn u16_at(b: &[u8], at: usize) -> Result<u16> {
    Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?))
}
fn u32_at(b: &[u8], at: usize) -> Result<u32> {
    Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?))
}
fn u64_at(b: &[u8], at: usize) -> Result<u64> {
    Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?))
}
fn size_at(b: &[u8], at: usize) -> Result<usize> {
    usize::try_from(u64_at(b, at)?).context("Nutzlastlänge nicht darstellbar")
}
fn put64(b: &mut [u8], at: usize, n: usize) {
    b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
}

pub use tb_vm::bytecode::checksum;

fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
    let end = 32usize
        .checked_add(u32_at(b, 20)? as usize)
        .context("Mach-O-Headerlänge")?;
    part(b, 0, end)?;
    let mut at = 32;
    let mut commands = Vec::new();
    for _ in 0..u32_at(b, 16)? {
        ensure!(at + 8 <= end, "Ungültige Mach-O-Ladekommandos");
        let cmd = u32_at(b, at)?;
        let len = u32_at(b, at + 4)? as usize;
        ensure!(
            len >= 8 && len.is_multiple_of(8) && len <= end - at,
            "Ungültige Mach-O-Kommandolänge"
        );
        commands.push((cmd, at));
        at += len;
    }
    ensure!(at == end, "Mach-O-Kommandotabelle unvollständig");
    Ok(commands)
}

/// Format-/Architekturprüfung; synthetische Header sind kein Zielausführungsnachweis.
pub fn native_target(b: &[u8]) -> Result<Target> {
    part(b, 0, 64)?;
    if b.starts_with(b"MZ") {
        let pe = u32_at(b, 0x3c)? as usize;
        ensure!(part(b, pe, 4)? == b"PE\0\0", "Ungültiges PE-Format");
        ensure!(
            u16_at(b, pe + 4)? == 0x8664,
            "PE-Architektur muss amd64 sein"
        );
        ensure!(
            u16_at(b, pe + 24)? == 0x20b && u16_at(b, pe + 92)? == 3,
            "PE muss PE32+ Terminalanwendung sein"
        );
        ensure!(
            u16_at(b, pe + 22)? & 0x2002 == 2,
            "PE muss Executable statt DLL sein"
        );
        return Ok(Target::WindowsAmd64);
    }
    if b.starts_with(b"\x7fELF") {
        ensure!(
            matches!(b[7], 0 | 3),
            "ELF-System-ABI muss System V/Linux sein"
        );
        ensure!(
            part(b, 4, 3)? == [2, 1, 1],
            "ELF muss 64-Bit Little Endian sein"
        );
        ensure!(matches!(u16_at(b, 16)?, 2 | 3), "ELF muss ausführbar sein");
        part(b, 0, 64)?;
        return match u16_at(b, 18)? {
            62 => Ok(Target::LinuxAmd64),
            183 => Ok(Target::LinuxArm64),
            _ => bail!("Nicht unterstützte ELF-Architektur"),
        };
    }
    if b.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) {
        ensure!(
            u32_at(b, 4)? == 0x0100000c,
            "Mach-O-Architektur muss arm64 sein"
        );
        ensure!(u32_at(b, 12)? == 2, "Mach-O muss Executable sein");
        macho_commands(b)?;
        return Ok(Target::MacosArm64);
    }
    bail!("Unbekanntes natives Zielformat (PE/ELF/Mach-O erwartet)")
}

/// Mach-O-Signaturen folgen der Nutzlast. Ihr Ladekommando ist die stabile Grenze.
fn content_end(b: &[u8], target: Target) -> Result<usize> {
    if target == Target::MacosArm64 {
        let signatures: Vec<_> = macho_commands(b)?
            .into_iter()
            .filter(|(cmd, _)| *cmd == 0x1d)
            .collect();
        ensure!(signatures.len() <= 1, "Doppelte Mach-O-Signatur");
        if let Some((_, at)) = signatures.first() {
            ensure!(u32_at(b, at + 4)? == 16, "Ungültiges Signaturkommando");
            let off = u32_at(b, at + 8)? as usize;
            let len = u32_at(b, at + 12)? as usize;
            part(b, off, len)?;
            ensure!(off + len == b.len(), "Daten hinter Mach-O-Signatur");
            return Ok(off);
        }
    }
    Ok(b.len())
}

pub fn embedded(b: &[u8], expected: Target) -> Result<CompiledModule> {
    ensure!(
        native_target(b)? == expected,
        "Executable-Target passt nicht zum Host"
    );
    let end = content_end(b, expected)?;
    // codesign richtet die Signatur auf 16 Bytes aus; ausschließlich Nullpadding zulassen.
    let footer = (0..=15)
        .find_map(|padding| {
            let at = end.checked_sub(FOOTER + padding)?;
            (b.get(at..at + 8) == Some(MAGIC) && b[at + FOOTER..end].iter().all(|v| *v == 0))
                .then_some(at)
        })
        .context("Fehlende oder beschädigte eingebettete Nutzlast (leere tbrt-Vorlage)")?;
    ensure!(
        u32_at(b, footer + 8)? == CONTAINER_VERSION,
        "Inkompatible Container-Version"
    );
    ensure!(
        u32_at(b, footer + 12)? == RUNTIME_VERSION,
        "Inkompatible Runtime-Version"
    );
    ensure!(
        u32_at(b, footer + 16)? == u32::from(TBC_VERSION),
        "Inkompatible TBC-Version"
    );
    ensure!(
        u32_at(b, footer + 20)? == expected as u32,
        "Nutzlast-Target passt nicht zum Executable"
    );
    let offset = size_at(b, footer + 24)?;
    let len = size_at(b, footer + 32)?;
    ensure!(
        offset >= 64 && offset <= footer && len == footer - offset,
        "Ungültige Nutzlastgrenzen"
    );
    let payload = part(b, offset, len)?;
    ensure!(
        checksum(payload) == u64_at(b, footer + 40)?,
        "Nutzlast-Prüfsumme stimmt nicht"
    );
    CompiledModule::from_tbc(payload)
        .map_err(|e| anyhow::anyhow!("Eingebettetes Programm nicht ladbar: {e}"))
}

/// Im nativen Runner vorhanden; verhindert Verwechslung mit beliebigen Executables.
pub const fn runtime_marker(target: Target) -> [u8; 32] {
    let mut marker = [0; 32];
    let magic = *b"TBRT-RUNTIME-v1!";
    let mut i = 0;
    while i < 16 {
        marker[i] = magic[i];
        i += 1;
    }
    let values = [
        CONTAINER_VERSION,
        RUNTIME_VERSION,
        TBC_VERSION as u32,
        target as u32,
    ];
    i = 0;
    while i < 4 {
        let bytes = values[i].to_le_bytes();
        let mut j = 0;
        while j < 4 {
            marker[16 + i * 4 + j] = bytes[j];
            j += 1;
        }
        i += 1;
    }
    marker
}
fn check_runner(b: &[u8], target: Target) -> Result<()> {
    ensure!(
        b.windows(32).any(|w| w == runtime_marker(target)),
        "Keine kompatible tbrt-Runtime-Vorlage (Runtime-/Format-/Target-Markierung fehlt)"
    );
    Ok(())
}

fn manifest(b: &[u8], target: Target) -> String {
    format!("TBRT-TEMPLATE 1\nruntime={RUNTIME_VERSION}\npackage={}\ntbc={TBC_VERSION}\ntarget={}\nchecksum={:016x}\n",env!("CARGO_PKG_VERSION"),target.triple(),checksum(b))
}
pub fn manifest_path(path: &Path) -> PathBuf {
    let mut p = path.as_os_str().to_owned();
    p.push(".meta");
    PathBuf::from(p)
}
/// Für den Runtime-Builder; kein impliziter Build beim Export.
pub fn prepare_template(path: &Path, target: Target) -> Result<()> {
    let b = fs::read(path)?;
    check_runner(&b, target)?;
    ensure!(
        native_target(&b)? == target,
        "Vorlagen-Zielformat/Architektur widerspricht {}",
        target.triple()
    );
    ensure!(
        embedded(&b, target).is_err(),
        "Bereits eingebettetes Programm ist keine leere Vorlage"
    );
    publish(
        &manifest_path(path),
        false,
        &[path.to_path_buf()],
        &|| false,
        |temp| {
            fs::write(temp, manifest(&b, target))?;
            Ok(())
        },
    )
}
fn template(path: &Path, target: Target) -> Result<Vec<u8>> {
    let b = fs::read(path).with_context(|| format!("Runtime-Vorlage fehlt: {}", path.display()))?;
    ensure!(
        native_target(&b)? == target,
        "Vorlagen-Zielformat/Architektur passt nicht zu {}",
        target.triple()
    );
    let found = fs::read_to_string(manifest_path(path)).context("Vorlagenmetadaten fehlen")?;
    check_runner(&b, target)?;
    let want = manifest(&b, target);
    for (index, (a, z)) in found.lines().zip(want.lines()).enumerate() {
        ensure!(
            a == z,
            "Inkompatible Vorlagenmetadaten, Zeile {}: Soll {z}, Ist {a}",
            index + 1
        );
    }
    ensure!(
        found == want,
        "Unvollständige Vorlagenmetadaten oder Integritätsdaten"
    );
    Ok(b)
}

static NEXT: AtomicU64 = AtomicU64::new(0);
struct Temporary(PathBuf);
impl Drop for Temporary {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.0);
    }
}
fn destination(path: &Path, protected: &[PathBuf]) -> Result<Option<Vec<u8>>> {
    let key = tb_vm::project_io::identity(path)?;
    for source in protected {
        ensure!(
            tb_vm::project_io::identity(source)? != key,
            "Ausgabe würde Projektdaten/Vorlage überschreiben: {}",
            path.display()
        );
    }
    match fs::symlink_metadata(path) {
        Ok(meta) => {
            ensure!(
                meta.is_file() && !meta.permissions().readonly(),
                "Ziel ist keine beschreibbare reguläre Datei: {}",
                path.display()
            );
            Ok(Some(fs::read(path)?))
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(e.into()),
    }
}
/// No-clobber per hard_link; Ersetzen nur nach expliziter Freigabe und unverändertem Ziel.
/// prepare darf finalisieren; cancellation wird vor Arbeit und Veröffentlichung geprüft.
pub fn publish(
    path: &Path,
    overwrite: bool,
    protected: &[PathBuf],
    cancelled: &dyn Fn() -> bool,
    prepare: impl FnOnce(&Path) -> Result<()>,
) -> Result<()> {
    ensure!(!cancelled(), "Export abgebrochen");
    let initial = destination(path, protected)?;
    ensure!(
        initial.is_none() || overwrite,
        "Ziel existiert; --force zum Ersetzen: {}",
        path.display()
    );
    let parent = path
        .parent()
        .filter(|p| !p.as_os_str().is_empty())
        .unwrap_or(Path::new("."));
    let temporary = loop {
        let p = parent.join(format!(
            ".tb-export-{}-{}.tmp",
            std::process::id(),
            NEXT.fetch_add(1, Ordering::Relaxed)
        ));
        match fs::OpenOptions::new().write(true).create_new(true).open(&p) {
            Ok(_) => break Temporary(p),
            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
            Err(e) => return Err(e.into()),
        }
    };
    prepare(&temporary.0)?;
    fs::File::open(&temporary.0)?.sync_all()?;
    ensure!(!cancelled(), "Export abgebrochen");
    ensure!(
        destination(path, protected)? == initial,
        "Ziel wurde während des Exports verändert"
    );
    if initial.is_some() {
        fs::rename(&temporary.0, path)?;
    } else {
        fs::hard_link(&temporary.0, path)
            .context("Ziel inzwischen belegt oder Veröffentlichung nicht möglich")?;
    }
    Ok(())
}

fn codesign(path: &Path, args: &[&str]) -> Result<()> {
    ensure!(
        cfg!(target_os = "macos"),
        "macOS-Finalisierung benötigt macOS mit /usr/bin/codesign"
    );
    let result = std::process::Command::new("/usr/bin/codesign")
        .args(args)
        .arg(path)
        .output()
        .context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?;
    ensure!(
        result.status.success(),
        "Finalisierung fehlgeschlagen: {}",
        String::from_utf8_lossy(&result.stderr)
    );
    Ok(())
}
fn append_payload(b: &mut Vec<u8>, module: &CompiledModule, target: Target) -> Result<()> {
    module.validate().map_err(|e| anyhow::anyhow!("{e}"))?;
    if target == Target::WindowsAmd64 {
        let pe = u32_at(b, 0x3c)? as usize;
        // Eine bestehende Authenticode-Signatur wäre nach dem Einbetten ungültig.
        if u32_at(b, pe + 132)? > 4 {
            ensure!(
                part(b, pe + 168, 8)?.iter().all(|v| *v == 0),
                "Signierte PE-Vorlagen werden nicht unterstützt; unsigned tbrt benötigt"
            );
        }
        // Für Benutzerprogramme optional; keine veraltete PE-Prüfsumme übernehmen.
        b[pe + 88..pe + 92].fill(0);
    }
    let payload = module.to_tbc();
    let offset = b.len();
    b.extend_from_slice(&payload);
    b.extend_from_slice(MAGIC);
    for n in [
        CONTAINER_VERSION,
        RUNTIME_VERSION,
        u32::from(TBC_VERSION),
        target as u32,
    ] {
        b.extend_from_slice(&n.to_le_bytes());
    }
    for n in [offset as u64, payload.len() as u64, checksum(&payload)] {
        b.extend_from_slice(&n.to_le_bytes());
    }
    if target == Target::MacosArm64 {
        let link = macho_commands(b)?
            .into_iter()
            .find(|(cmd, at)| {
                *cmd == 0x19 && b.get(at + 8..at + 24) == Some(b"__LINKEDIT\0\0\0\0\0\0")
            })
            .context("Mach-O __LINKEDIT fehlt")?
            .1;
        ensure!(
            u32_at(b, link + 4)? as usize >= 72,
            "Mach-O-Segment zu kurz"
        );
        let start = size_at(b, link + 40)?;
        ensure!(start <= offset, "Ungültige __LINKEDIT-Grenze");
        let len = b.len() - start;
        put64(b, link + 48, len);
        put64(
            b,
            link + 32,
            len.checked_add(16383).context("Mach-O-Länge")? & !16383,
        );
    }
    Ok(())
}

pub struct Export<'a> {
    pub module: &'a CompiledModule,
    pub target: Target,
    pub template: &'a Path,
    pub output: &'a Path,
    pub overwrite: bool,
    pub protected: &'a [PathBuf],
}
pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
    let mut b = template(request.template, request.target)?;
    let mut protected = request.protected.to_vec();
    protected.extend([
        request.template.to_path_buf(),
        manifest_path(request.template),
    ]);
    protected.extend(
        request
            .module
            .sources
            .iter()
            .map(|s| PathBuf::from(&s.path)),
    );
    publish(
        request.output,
        request.overwrite,
        &protected,
        cancelled,
        |temp| {
            if request.target == Target::MacosArm64 {
                fs::write(temp, &b)?;
                codesign(temp, &["--remove-signature"])?;
                b = fs::read(temp)?;
            }
            append_payload(&mut b, request.module, request.target)?;
            fs::write(temp, &b)?;
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?;
            }
            if request.target == Target::MacosArm64 {
                codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?;
                codesign(temp, &["--verify", "--strict"])?;
            }
            embedded(&fs::read(temp)?, request.target)?;
            Ok(())
        },
    )
}

#[cfg(test)]
mod tests;
