use std::env;
use std::error::Error;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Duration;

use sha2::Digest;

use crate::archive::{extract_artifact_archive, move_dir, TempDir};
use crate::args::{
    download_spec_for_language, ArtifactLanguage, CacheCliOptions, CliError, DownloadCliOptions,
    WhereCliOptions,
};
use crate::commands::unidic_artifact::verify_unidic_artifact_bundle;
use crate::commands::zh_artifact::verify_zh_artifact_bundle;

const DOWNLOAD_TIMEOUT_SECS: u64 = 60;
pub(crate) const MAX_DOWNLOAD_BYTES: u64 = 512 * 1024 * 1024;
pub(crate) const MAX_CHECKSUM_MANIFEST_BYTES: u64 = 1024 * 1024;

pub(crate) fn run_download(options: DownloadCliOptions) -> Result<(), Box<dyn Error>> {
    let cache_dir = options
        .cache_dir
        .as_deref()
        .map(PathBuf::from)
        .unwrap_or_else(default_cache_dir);
    let archive_url = options.url.as_deref().unwrap_or(options.spec.archive_url);
    let archive_name = uri_file_name(archive_url).unwrap_or(options.spec.archive_name);
    let temp = TempDir::new("moine-download")?;
    let archive_path = temp.path().join(archive_name);

    copy_uri_to_path(archive_url, &archive_path)?;
    if let Some(expected_sha256) = download_expected_sha256(&options, archive_name)? {
        let actual_sha256 = sha256_file(&archive_path)?;
        if actual_sha256 != expected_sha256 {
            return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                "checksum mismatch for {archive_name}: expected {expected_sha256}, got {actual_sha256}"
            ))));
        }
    }

    fs::create_dir_all(&cache_dir)?;
    let staging = TempDir::new_in(&cache_dir, ".moine-install")?;
    let extracted_root = extract_artifact_archive(&archive_path, &staging.path().join("extract"))?;
    let metadata = extracted_root.join("metadata.yaml");
    if !metadata.is_file() {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "downloaded artifact has no metadata.yaml: {}",
            extracted_root.display()
        ))));
    }
    verify_downloaded_bundle(options.spec.language, &metadata)?;

    let destination = cache_dir.join(extracted_root.file_name().ok_or_else(|| {
        CliError::ArtifactVerificationFailed(format!(
            "extracted artifact root {} has no file name",
            extracted_root.display()
        ))
    })?);
    if destination.exists() {
        if !options.force {
            verify_existing_installed_bundle(options.spec.language, &destination)?;
            println!("{}", destination.display());
            return Ok(());
        }
        replace_dir(&extracted_root, &destination)?;
    } else {
        move_dir(&extracted_root, &destination)?;
    }
    println!("{}", destination.display());
    Ok(())
}

pub(crate) fn run_download_list(options: CacheCliOptions) -> Result<(), Box<dyn Error>> {
    let cache_dir = options
        .cache_dir
        .map(PathBuf::from)
        .unwrap_or_else(default_cache_dir);
    for metadata in installed_metadata_paths(&cache_dir)? {
        if let Some(parent) = metadata.parent() {
            println!("{}", parent.display());
        }
    }
    Ok(())
}

pub(crate) fn run_download_where(options: WhereCliOptions) -> Result<(), Box<dyn Error>> {
    let cache_dir = options
        .cache_dir
        .map(PathBuf::from)
        .unwrap_or_else(default_cache_dir);
    let Some(language) = options.language else {
        println!("{}", cache_dir.display());
        return Ok(());
    };
    let spec = download_spec_for_language(language);
    if let Some(metadata) = find_metadata_by_prefix(&cache_dir, spec.artifact_name)? {
        if let Some(parent) = metadata.parent() {
            println!("{}", parent.display());
            return Ok(());
        }
    }
    println!("{}", cache_dir.join(spec.artifact_name).display());
    Ok(())
}

pub(crate) fn default_cache_dir() -> PathBuf {
    if let Some(cache_dir) = env::var_os("MOINE_CACHE_DIR") {
        return PathBuf::from(cache_dir);
    }
    if let Some(cache_home) = env::var_os("XDG_CACHE_HOME") {
        return PathBuf::from(cache_home).join("moine").join("dictionaries");
    }
    if let Some(home) = env::var_os("HOME") {
        return PathBuf::from(home)
            .join(".cache")
            .join("moine")
            .join("dictionaries");
    }
    PathBuf::from(".moine").join("dictionaries")
}

pub(crate) fn uri_file_name(uri: &str) -> Option<&str> {
    uri.rsplit('/')
        .next()
        .filter(|name| !name.is_empty() && !name.contains('\\'))
}

pub(crate) fn copy_uri_to_path(uri: &str, output: &Path) -> Result<(), Box<dyn Error>> {
    if uri.starts_with("http://") || uri.starts_with("https://") {
        let response = ureq::get(uri)
            .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECS))
            .call()?;
        return copy_reader_to_path_limited(response.into_reader(), output);
    }
    if let Some(path) = uri.strip_prefix("file://") {
        return copy_file_to_path_limited(Path::new(path), output);
    }
    copy_file_to_path_limited(Path::new(uri), output)
}

fn copy_file_to_path_limited(path: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
    let file = fs::File::open(path)?;
    let file_size = file.metadata()?.len();
    if file_size > MAX_DOWNLOAD_BYTES {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "download exceeded maximum size of {MAX_DOWNLOAD_BYTES} bytes"
        ))));
    }
    copy_reader_to_path_limited(file, output)
}

fn copy_reader_to_path_limited(mut reader: impl Read, output: &Path) -> Result<(), Box<dyn Error>> {
    let mut file = fs::File::create(output)?;
    let copied = std::io::copy(&mut reader.by_ref().take(MAX_DOWNLOAD_BYTES + 1), &mut file)?;
    if copied > MAX_DOWNLOAD_BYTES {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "download exceeded maximum size of {MAX_DOWNLOAD_BYTES} bytes"
        ))));
    }
    Ok(())
}

pub(crate) fn read_uri_text(uri: &str) -> Result<String, Box<dyn Error>> {
    if uri.starts_with("http://") || uri.starts_with("https://") {
        let response = ureq::get(uri)
            .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECS))
            .call()?;
        return read_text_limited(response.into_reader());
    }
    if let Some(path) = uri.strip_prefix("file://") {
        return read_file_text_limited(Path::new(path));
    }
    read_file_text_limited(Path::new(uri))
}

fn read_file_text_limited(path: &Path) -> Result<String, Box<dyn Error>> {
    let file = fs::File::open(path)?;
    let file_size = file.metadata()?.len();
    if file_size > MAX_CHECKSUM_MANIFEST_BYTES {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "checksum manifest exceeded maximum size of {MAX_CHECKSUM_MANIFEST_BYTES} bytes"
        ))));
    }
    read_text_limited(file)
}

fn read_text_limited(mut reader: impl Read) -> Result<String, Box<dyn Error>> {
    let mut text = String::new();
    let read = reader
        .by_ref()
        .take(MAX_CHECKSUM_MANIFEST_BYTES + 1)
        .read_to_string(&mut text)?;
    if read as u64 > MAX_CHECKSUM_MANIFEST_BYTES {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "checksum manifest exceeded maximum size of {MAX_CHECKSUM_MANIFEST_BYTES} bytes"
        ))));
    }
    Ok(text)
}

pub(crate) fn expected_sha256(
    checksum_url: &str,
    archive_name: &str,
) -> Result<String, Box<dyn Error>> {
    for line in read_uri_text(checksum_url)?.lines() {
        let mut parts = line.split_whitespace();
        let Some(digest) = parts.next() else {
            continue;
        };
        let Some(label) = parts.next() else {
            continue;
        };
        if parts.next().is_some() {
            continue;
        }
        if label == archive_name
            || Path::new(label).file_name().and_then(|name| name.to_str()) == Some(archive_name)
        {
            return Ok(digest.to_ascii_lowercase());
        }
    }
    Err(Box::new(CliError::ArtifactVerificationFailed(format!(
        "{archive_name} not found in checksum manifest: {checksum_url}"
    ))))
}

pub(crate) fn download_expected_sha256(
    options: &DownloadCliOptions,
    archive_name: &str,
) -> Result<Option<String>, Box<dyn Error>> {
    if let Some(value) = &options.sha256 {
        return Ok(Some(value.to_ascii_lowercase()));
    }
    let checksum_url = options.checksum_url.as_deref().or_else(|| {
        options
            .url
            .is_none()
            .then_some(options.spec.checksum_url)
            .flatten()
    });
    if let Some(checksum_url) = checksum_url {
        return Ok(Some(expected_sha256(checksum_url, archive_name)?));
    }
    Ok(None)
}

pub(crate) fn sha256_file(path: &Path) -> Result<String, Box<dyn Error>> {
    let mut file = fs::File::open(path)?;
    let mut digest = sha2::Sha256::new();
    let mut buffer = [0_u8; 1024 * 64];
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        sha2::Digest::update(&mut digest, &buffer[..read]);
    }
    Ok(format!("{:x}", sha2::Digest::finalize(digest)))
}

pub(crate) fn verify_downloaded_bundle(
    language: ArtifactLanguage,
    metadata: &Path,
) -> Result<(), Box<dyn Error>> {
    let metadata = metadata.to_str().ok_or_else(|| {
        CliError::ArtifactVerificationFailed("metadata path is not UTF-8".to_string())
    })?;
    match language {
        ArtifactLanguage::Japanese | ArtifactLanguage::JapaneseSudachi => {
            let verified = verify_unidic_artifact_bundle(metadata, None, false)?;
            verify_japanese_download_identity(language, &verified.metadata)?;
        }
        ArtifactLanguage::Chinese => {
            verify_zh_artifact_bundle(metadata, None)?;
        }
    }
    Ok(())
}

fn verify_existing_installed_bundle(
    language: ArtifactLanguage,
    destination: &Path,
) -> Result<(), Box<dyn Error>> {
    let metadata = destination.join("metadata.yaml");
    if !metadata.is_file() {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "installed artifact has no metadata.yaml: {}",
            destination.display()
        ))));
    }
    verify_downloaded_bundle(language, &metadata)
}

fn replace_dir(source: &Path, destination: &Path) -> Result<(), Box<dyn Error>> {
    let backup = replacement_backup_path(destination)?;
    fs::rename(destination, &backup)?;
    match move_dir(source, destination) {
        Ok(()) => {
            remove_path(&backup)?;
            Ok(())
        }
        Err(err) => {
            let _ = remove_path(destination);
            let _ = fs::rename(&backup, destination);
            Err(err)
        }
    }
}

fn remove_path(path: &Path) -> Result<(), Box<dyn Error>> {
    let metadata = fs::symlink_metadata(path)?;
    if metadata.file_type().is_dir() {
        fs::remove_dir_all(path)?;
    } else {
        fs::remove_file(path)?;
    }
    Ok(())
}

fn replacement_backup_path(destination: &Path) -> Result<PathBuf, Box<dyn Error>> {
    let parent = destination.parent().ok_or_else(|| {
        CliError::ArtifactVerificationFailed(format!(
            "installed artifact {} has no parent directory",
            destination.display()
        ))
    })?;
    let name = destination
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| {
            CliError::ArtifactVerificationFailed(format!(
                "installed artifact path is not UTF-8: {}",
                destination.display()
            ))
        })?;
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    for index in 0..100 {
        let candidate = parent.join(format!(
            ".{name}.replacing-{}-{nanos}-{index}",
            std::process::id()
        ));
        if !candidate.exists() {
            return Ok(candidate);
        }
    }
    Err(Box::new(CliError::ArtifactVerificationFailed(format!(
        "could not choose replacement path for {}",
        destination.display()
    ))))
}

fn verify_japanese_download_identity(
    language: ArtifactLanguage,
    metadata: &moine_ja::UnidicArtifactMetadata,
) -> Result<(), Box<dyn Error>> {
    match language {
        ArtifactLanguage::Japanese => {
            if metadata.artifact_name.starts_with("moine-sudachi")
                || metadata.source.name != "UniDic-CWJ"
                || metadata.build.reading_field == "sudachi-reading"
            {
                return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                    "download ja requires a UniDic-CWJ artifact; got {} from {}",
                    metadata.artifact_name, metadata.source.name
                ))));
            }
        }
        ArtifactLanguage::JapaneseSudachi => {
            if metadata.artifact_name.starts_with("moine-unidic")
                || metadata.source.name != "SudachiDict"
                || metadata.build.reading_field != "sudachi-reading"
            {
                return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                    "download ja-sudachi requires a SudachiDict artifact; got {} from {}",
                    metadata.artifact_name, metadata.source.name
                ))));
            }
        }
        ArtifactLanguage::Chinese => {}
    }
    Ok(())
}

pub(crate) fn installed_metadata_paths(cache_dir: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let mut metadata_paths = Vec::new();
    if !cache_dir.is_dir() {
        return Ok(metadata_paths);
    }
    let root_metadata = cache_dir.join("metadata.yaml");
    if root_metadata.is_file() {
        metadata_paths.push(root_metadata);
    }
    for entry in fs::read_dir(cache_dir)? {
        let path = entry?.path();
        let metadata = path.join("metadata.yaml");
        if path.is_dir() && metadata.is_file() {
            metadata_paths.push(metadata);
        }
    }
    metadata_paths.sort();
    metadata_paths.dedup();
    Ok(metadata_paths)
}

pub(crate) fn find_metadata_by_prefix(
    cache_dir: &Path,
    artifact_name: &str,
) -> Result<Option<PathBuf>, Box<dyn Error>> {
    Ok(installed_metadata_paths(cache_dir)?
        .into_iter()
        .find(|metadata| {
            metadata
                .parent()
                .and_then(|parent| parent.file_name())
                .and_then(|name| name.to_str())
                .is_some_and(|name| name.starts_with(artifact_name))
        }))
}
