use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

use crate::constants::{THESA_FORMAT_VERSION, THESA_METADATA_DIR};
use crate::error::{Result, ThesaError};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ArchiveManifest {
    pub(crate) format_version: String,
    pub(crate) tool: ManifestTool,
    pub(crate) source: ManifestSource,
    pub(crate) archive: ManifestArchive,
    pub(crate) files: Vec<ManifestFile>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ManifestTool {
    pub(crate) name: String,
    pub(crate) version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ManifestSource {
    pub(crate) platform: String,
    pub(crate) target: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ManifestArchive {
    pub(crate) created_at_unix: u64,
    pub(crate) complete: bool,
    pub(crate) file_count: usize,
    pub(crate) total_bytes: u64,
    pub(crate) checksum_algorithm: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ManifestFile {
    pub(crate) path: String,
    pub(crate) size: u64,
    #[serde(default)]
    pub(crate) modified_unix_nanos: Option<u64>,
    pub(crate) blake3: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct VerifyReport {
    pub(crate) archive: String,
    pub(crate) ok: bool,
    pub(crate) files_verified: usize,
    pub(crate) bytes_verified: u64,
    pub(crate) manifest_valid: bool,
    pub(crate) checksums_valid: bool,
    pub(crate) path_safety_valid: bool,
    pub(crate) warnings: Vec<String>,
    pub(crate) errors: Vec<String>,
}

pub(crate) fn prepare_output_dir(output: &Path) -> Result<PathBuf> {
    if output.exists() && !output.is_dir() {
        return Err(ThesaError::OutputNotDirectory(output.display().to_string()));
    }
    fs::create_dir_all(output)?;
    Ok(output.to_path_buf())
}

pub(crate) fn write_archive_manifest(
    output: &Path,
    platform: &str,
    target: &str,
    complete: bool,
) -> Result<ArchiveManifest> {
    let previous_manifest = read_archive_manifest_sidecar(output).ok();
    let manifest = build_archive_manifest_with_previous(
        output,
        platform,
        target,
        complete,
        previous_manifest.as_ref(),
    )?;
    let metadata_dir = output.join(THESA_METADATA_DIR);
    fs::create_dir_all(&metadata_dir)?;

    let manifest_path = metadata_dir.join("manifest.json");
    let tmp_manifest_path = metadata_dir.join("manifest.json.tmp");
    let manifest_json = serde_json::to_string(&manifest)
        .map_err(|err| ThesaError::Message(format!("manifest serialization failed: {err}")))?;
    fs::write(&tmp_manifest_path, manifest_json)?;
    fs::rename(&tmp_manifest_path, &manifest_path)?;

    let checksums_path = metadata_dir.join("checksums.blake3");
    let tmp_checksums_path = metadata_dir.join("checksums.blake3.tmp");
    let mut checksums = String::new();
    for file in &manifest.files {
        checksums.push_str(&file.blake3);
        checksums.push_str("  ");
        checksums.push_str(&file.path);
        checksums.push('\n');
    }
    fs::write(&tmp_checksums_path, checksums)?;
    fs::rename(&tmp_checksums_path, &checksums_path)?;

    Ok(manifest)
}

#[cfg(test)]
pub(crate) fn build_archive_manifest(
    output: &Path,
    platform: &str,
    target: &str,
    complete: bool,
) -> Result<ArchiveManifest> {
    build_archive_manifest_with_previous(output, platform, target, complete, None)
}

fn build_archive_manifest_with_previous(
    output: &Path,
    platform: &str,
    target: &str,
    complete: bool,
    previous: Option<&ArchiveManifest>,
) -> Result<ArchiveManifest> {
    let mut files = collect_manifest_files(output, previous)?;
    files.sort_by(|a, b| a.path.cmp(&b.path));
    let total_bytes = files.iter().map(|file| file.size).sum();
    let created_at_unix = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|err| ThesaError::Message(format!("system clock is before UNIX epoch: {err}")))?
        .as_secs();

    Ok(ArchiveManifest {
        format_version: THESA_FORMAT_VERSION.to_string(),
        tool: ManifestTool {
            name: "thesa".to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
        },
        source: ManifestSource {
            platform: platform.to_string(),
            target: target.to_string(),
        },
        archive: ManifestArchive {
            created_at_unix,
            complete,
            file_count: files.len(),
            total_bytes,
            checksum_algorithm: "blake3".to_string(),
        },
        files,
    })
}

fn collect_manifest_files(
    root: &Path,
    previous: Option<&ArchiveManifest>,
) -> Result<Vec<ManifestFile>> {
    let mut files = Vec::new();
    let previous = previous_manifest_file_map(previous);
    collect_manifest_files_inner(root, root, &previous, &mut files)?;
    Ok(files)
}

fn previous_manifest_file_map(
    previous: Option<&ArchiveManifest>,
) -> BTreeMap<String, ManifestFile> {
    previous
        .into_iter()
        .flat_map(|manifest| manifest.files.iter().cloned())
        .filter(|file| validate_manifest_relative_path(&file.path).is_ok())
        .map(|file| (file.path.clone(), file))
        .collect()
}

fn collect_manifest_files_inner(
    root: &Path,
    current: &Path,
    previous: &BTreeMap<String, ManifestFile>,
    files: &mut Vec<ManifestFile>,
) -> Result<()> {
    for entry in fs::read_dir(current)? {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name();
        if path.is_dir() {
            if name == THESA_METADATA_DIR {
                continue;
            }
            collect_manifest_files_inner(root, &path, previous, files)?;
            continue;
        }
        if !path.is_file() {
            continue;
        }

        let metadata = fs::metadata(&path)?;
        let relative = path
            .strip_prefix(root)
            .map_err(|err| ThesaError::Message(format!("manifest path error: {err}")))?;
        let path = manifest_path(relative)?;
        let modified_unix_nanos = metadata_modified_unix_nanos(&metadata);
        let blake3 = if let Some(cached) = previous.get(&path) {
            if cached.size == metadata.len() && cached.modified_unix_nanos == modified_unix_nanos {
                cached.blake3.clone()
            } else {
                blake3_hash_file(&path_from_root(root, &path)?)?
            }
        } else {
            blake3_hash_file(&path_from_root(root, &path)?)?
        };
        files.push(ManifestFile {
            path,
            size: metadata.len(),
            modified_unix_nanos,
            blake3,
        });
    }
    Ok(())
}

fn path_from_root(root: &Path, manifest_path: &str) -> Result<PathBuf> {
    let relative = validate_manifest_relative_path(manifest_path).map_err(|message| {
        ThesaError::Message(format!(
            "invalid generated manifest path '{manifest_path}': {message}"
        ))
    })?;
    Ok(root.join(relative))
}

fn metadata_modified_unix_nanos(metadata: &fs::Metadata) -> Option<u64> {
    metadata
        .modified()
        .ok()
        .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
        .and_then(|duration| u64::try_from(duration.as_nanos()).ok())
}

fn blake3_hash_file(path: &Path) -> Result<String> {
    let mut file = fs::File::open(path)?;
    let mut hasher = blake3::Hasher::new();
    io::copy(&mut file, &mut hasher)?;
    Ok(hasher.finalize().to_hex().to_string())
}

fn manifest_path(path: &Path) -> Result<String> {
    let normalized = path
        .components()
        .map(|component| component.as_os_str().to_string_lossy())
        .collect::<Vec<_>>()
        .join("/");
    validate_manifest_relative_path(&normalized).map_err(|message| {
        ThesaError::Message(format!(
            "refusing to write unsafe manifest path '{normalized}': {message}"
        ))
    })?;
    Ok(normalized)
}

pub(crate) fn read_archive_manifest_sidecar(root: &Path) -> Result<ArchiveManifest> {
    let manifest_path = root.join(THESA_METADATA_DIR).join("manifest.json");
    let contents = fs::read_to_string(&manifest_path)?;
    serde_json::from_str::<ArchiveManifest>(&contents).map_err(|err| {
        ThesaError::Message(format!(
            "unable to parse existing manifest {}: {err}",
            manifest_path.display()
        ))
    })
}

pub(crate) fn run_verify_command(archive: &Path, json: bool) -> Result<()> {
    let report = verify_archive(archive);
    if json {
        let payload = serde_json::to_string_pretty(&report).map_err(|err| {
            ThesaError::Message(format!("verification report serialization failed: {err}"))
        })?;
        println!("{payload}");
    } else {
        print_verify_report(&report);
    }

    if report.ok {
        Ok(())
    } else {
        Err(ThesaError::Message(
            "archive verification failed".to_string(),
        ))
    }
}

pub(crate) fn verify_archive(input: &Path) -> VerifyReport {
    let archive_root = archive_root_from_input(input);
    let mut report = VerifyReport {
        archive: archive_root.display().to_string(),
        ok: false,
        files_verified: 0,
        bytes_verified: 0,
        manifest_valid: false,
        checksums_valid: false,
        path_safety_valid: true,
        warnings: Vec::new(),
        errors: Vec::new(),
    };

    if !archive_root.is_dir() {
        report.errors.push(format!(
            "archive path is not a directory: {}",
            archive_root.display()
        ));
        return report;
    }

    let metadata_dir = archive_root.join(THESA_METADATA_DIR);
    let manifest_path = metadata_dir.join("manifest.json");
    let manifest = match fs::read_to_string(&manifest_path) {
        Ok(contents) => match serde_json::from_str::<ArchiveManifest>(&contents) {
            Ok(manifest) => manifest,
            Err(err) => {
                report
                    .errors
                    .push(format!("manifest is not valid JSON: {err}"));
                return report;
            }
        },
        Err(err) => {
            report.errors.push(format!(
                "unable to read manifest {}: {err}",
                manifest_path.display()
            ));
            return report;
        }
    };
    report.manifest_valid = true;

    if manifest.format_version != THESA_FORMAT_VERSION {
        report.errors.push(format!(
            "unsupported archive format version '{}', expected '{}'",
            manifest.format_version, THESA_FORMAT_VERSION
        ));
    }
    if manifest.archive.checksum_algorithm != "blake3" {
        report.errors.push(format!(
            "unsupported checksum algorithm '{}', expected 'blake3'",
            manifest.archive.checksum_algorithm
        ));
    }
    if !manifest.archive.complete {
        report
            .errors
            .push("manifest marks this archive as incomplete".to_string());
    }
    if manifest.archive.file_count != manifest.files.len() {
        report.errors.push(format!(
            "manifest file_count {} does not match listed files {}",
            manifest.archive.file_count,
            manifest.files.len()
        ));
    }
    let manifest_total_bytes = manifest.files.iter().map(|file| file.size).sum::<u64>();
    if manifest.archive.total_bytes != manifest_total_bytes {
        report.errors.push(format!(
            "manifest total_bytes {} does not match listed file bytes {}",
            manifest.archive.total_bytes, manifest_total_bytes
        ));
    }

    let checksums_path = metadata_dir.join("checksums.blake3");
    let checksums = match read_checksum_sidecar(&checksums_path) {
        Ok(checksums) => {
            report.checksums_valid = true;
            checksums
        }
        Err(message) => {
            report.errors.push(message);
            BTreeMap::new()
        }
    };

    let mut manifest_paths = BTreeSet::new();
    for file in &manifest.files {
        if !manifest_paths.insert(file.path.clone()) {
            report
                .errors
                .push(format!("duplicate manifest path: {}", file.path));
            continue;
        }

        let relative = match validate_manifest_relative_path(&file.path) {
            Ok(relative) => relative,
            Err(message) => {
                report.path_safety_valid = false;
                report
                    .errors
                    .push(format!("unsafe manifest path '{}': {message}", file.path));
                continue;
            }
        };

        if let Some(sidecar_hash) = checksums.get(&file.path) {
            if !sidecar_hash.eq_ignore_ascii_case(&file.blake3) {
                report.errors.push(format!(
                    "checksum sidecar disagrees with manifest for {}",
                    file.path
                ));
                report.checksums_valid = false;
            }
        } else if !checksums.is_empty() {
            report
                .errors
                .push(format!("checksum sidecar is missing path {}", file.path));
            report.checksums_valid = false;
        }

        let path = archive_root.join(&relative);
        let metadata = match fs::metadata(&path) {
            Ok(metadata) if metadata.is_file() => metadata,
            Ok(_) => {
                report.errors.push(format!(
                    "manifest path is not a regular file: {}",
                    file.path
                ));
                continue;
            }
            Err(err) => {
                report
                    .errors
                    .push(format!("missing manifest file {}: {err}", file.path));
                continue;
            }
        };

        if metadata.len() != file.size {
            report.errors.push(format!(
                "size mismatch for {}: manifest {}, actual {}",
                file.path,
                file.size,
                metadata.len()
            ));
            continue;
        }

        if let Some(expected_mtime) = file.modified_unix_nanos {
            let actual_mtime = metadata_modified_unix_nanos(&metadata);
            if actual_mtime != Some(expected_mtime) {
                report.errors.push(format!(
                    "modified timestamp mismatch for {}: manifest {:?}, actual {:?}",
                    file.path,
                    Some(expected_mtime),
                    actual_mtime
                ));
                continue;
            }
        }

        match blake3_hash_file(&path) {
            Ok(actual) if actual.eq_ignore_ascii_case(&file.blake3) => {
                report.files_verified += 1;
                report.bytes_verified += file.size;
            }
            Ok(actual) => report.errors.push(format!(
                "checksum mismatch for {}: manifest {}, actual {}",
                file.path, file.blake3, actual
            )),
            Err(err) => report
                .errors
                .push(format!("unable to hash {}: {err}", file.path)),
        }
    }

    for path in checksums.keys() {
        if !manifest_paths.contains(path) {
            report.errors.push(format!(
                "checksum sidecar contains path not in manifest: {path}"
            ));
            report.checksums_valid = false;
        }
    }

    report.ok = report.errors.is_empty()
        && report.manifest_valid
        && report.checksums_valid
        && report.path_safety_valid
        && report.files_verified == manifest.files.len();
    report
}

fn archive_root_from_input(input: &Path) -> PathBuf {
    if input.file_name().and_then(|name| name.to_str()) == Some(THESA_METADATA_DIR) {
        input.parent().unwrap_or(input).to_path_buf()
    } else {
        input.to_path_buf()
    }
}

fn read_checksum_sidecar(path: &Path) -> std::result::Result<BTreeMap<String, String>, String> {
    let contents = fs::read_to_string(path)
        .map_err(|err| format!("unable to read checksum sidecar {}: {err}", path.display()))?;
    let mut checksums = BTreeMap::new();
    for (line_index, raw_line) in contents.lines().enumerate() {
        let line = raw_line.trim();
        if line.is_empty() {
            continue;
        }
        let (hash, path) = line.split_once("  ").ok_or_else(|| {
            format!(
                "invalid checksum sidecar line {}: expected '<blake3>  <path>'",
                line_index + 1
            )
        })?;
        if hash.len() != 64 || !hash.chars().all(|ch| ch.is_ascii_hexdigit()) {
            return Err(format!(
                "invalid BLAKE3 hash on checksum sidecar line {}",
                line_index + 1
            ));
        }
        validate_manifest_relative_path(path).map_err(|message| {
            format!(
                "unsafe path in checksum sidecar line {}: {path}: {message}",
                line_index + 1
            )
        })?;
        if checksums
            .insert(path.to_string(), hash.to_ascii_lowercase())
            .is_some()
        {
            return Err(format!(
                "duplicate path in checksum sidecar line {}: {path}",
                line_index + 1
            ));
        }
    }
    Ok(checksums)
}

fn validate_manifest_relative_path(path: &str) -> std::result::Result<PathBuf, String> {
    if path.is_empty() {
        return Err("path is empty".to_string());
    }
    if path.contains('\\') {
        return Err("backslashes are not allowed in manifest paths".to_string());
    }

    let path_ref = Path::new(path);
    if path_ref.is_absolute() {
        return Err("absolute paths are not allowed".to_string());
    }

    let mut safe = PathBuf::new();
    for (index, component) in path_ref.components().enumerate() {
        match component {
            Component::Normal(value) => {
                if index == 0 && value == THESA_METADATA_DIR {
                    return Err("manifest paths may not target .thesa metadata".to_string());
                }
                safe.push(value);
            }
            Component::CurDir => {
                return Err("current-directory components are not allowed".to_string());
            }
            Component::ParentDir => {
                return Err("parent-directory components are not allowed".to_string());
            }
            Component::RootDir | Component::Prefix(_) => {
                return Err("root or prefix components are not allowed".to_string());
            }
        }
    }
    if safe.as_os_str().is_empty() {
        return Err("path has no normal components".to_string());
    }
    Ok(safe)
}

fn print_verify_report(report: &VerifyReport) {
    println!("Archive: {}", report.archive);
    println!("Status: {}", if report.ok { "OK" } else { "FAILED" });
    println!("Files: {} verified", report.files_verified);
    println!("Bytes: {} verified", report.bytes_verified);
    println!(
        "Manifest: {}",
        if report.manifest_valid {
            "valid"
        } else {
            "invalid"
        }
    );
    println!(
        "Checksums: {}",
        if report.checksums_valid {
            "valid"
        } else {
            "invalid"
        }
    );
    println!(
        "Path safety: {}",
        if report.path_safety_valid {
            "valid"
        } else {
            "invalid"
        }
    );
    println!("Warnings: {}", report.warnings.len());
    for warning in &report.warnings {
        println!(" - warning: {warning}");
    }
    println!("Errors: {}", report.errors.len());
    for error in &report.errors {
        println!(" - error: {error}");
    }
}
