use std::error::Error;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};

use moine_zh::{
    artifact_file_digest_path as zh_artifact_file_digest_path, CedictIndexOptions,
    ZhArtifactLicense, ZhArtifactMetadata, ZhArtifactMetadataOptions, ZhReadingIndex,
    ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM as ZH_ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM,
    ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM as ZH_ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM,
};

use crate::archive::{
    checked_bundle_path, create_output_file, normalized_relative_archive_path,
    release_checksum_asset_label, write_output_file, write_release_archive, ArchiveEntry,
};
use crate::args::{
    default_zh_payload_file_name, ArtifactPayloadFormat, CliError, ZhArtifactArchiveCliOptions,
    ZhArtifactBundleCliOptions, ZhArtifactInspectCliOptions, ZhArtifactMetadataCliOptions,
    ZhArtifactPayloadCliOptions, ZhArtifactReleaseChecksumsCliOptions, ZhArtifactVerifyCliOptions,
    ZhIndexSource, INDEXED_PAYLOAD_FORMAT, YAML_PAYLOAD_FORMAT,
};

pub(crate) fn run_zh_artifact_metadata(
    options: ZhArtifactMetadataCliOptions,
) -> Result<(), Box<dyn Error>> {
    let index =
        ZhReadingIndex::from_cedict_path_with_options(&options.cedict, options.index_options)?;
    let metadata = index.artifact_metadata(ZhArtifactMetadataOptions {
        artifact_name: options.artifact_name,
        generator: "moine-cli".to_string(),
        payload_file_name: options.payload_file_name,
        payload_format: options.payload_format.as_str().to_string(),
        source_name: options.source_name,
        source_version: options.source_version,
        source_cedict: options.cedict.clone(),
        index_options: options.index_options,
        query_defaults: options.reading_options,
        license: ZhArtifactLicense::default(),
    });
    let yaml = serde_yaml::to_string(&metadata)?;

    if let Some(output) = &options.output {
        write_output_file(output, yaml)?;
    } else {
        print!("{yaml}");
    }

    Ok(())
}

pub(crate) fn run_zh_artifact_payload(
    options: ZhArtifactPayloadCliOptions,
) -> Result<(), Box<dyn Error>> {
    let index =
        ZhReadingIndex::from_cedict_path_with_options(&options.cedict, options.index_options)?;

    if let Some(output) = &options.output {
        write_zh_artifact_payload_file(&index, options.payload_format, Path::new(output))?;
    } else {
        if options.payload_format != ArtifactPayloadFormat::Yaml {
            return Err(Box::new(CliError::ArtifactVerificationFailed(
                "non-YAML zh payload output requires --output".to_string(),
            )));
        }
        print!("{}", serde_yaml::to_string(&index.artifact_payload())?);
    }

    Ok(())
}

pub(crate) fn run_zh_artifact_archive(
    options: ZhArtifactArchiveCliOptions,
) -> Result<(), Box<dyn Error>> {
    let verified = verify_zh_artifact_bundle(&options.metadata, options.bundle_dir.as_deref())?;
    let root_name = options
        .root_name
        .unwrap_or_else(|| verified.metadata.artifact_name.clone());
    let entries = zh_release_archive_entries(&verified)?;
    let output = create_output_file(&options.output)?;
    write_release_archive(output, options.compression, &root_name, &entries)?;

    println!("archive: {}", options.output);
    println!("compression: {}", options.compression.as_str());
    println!("root: {}", root_name);
    println!("metadata: {}", verified.metadata_path.display());
    println!("bundle: {}", verified.bundle_dir.display());
    println!("entries: {}", entries.len());
    println!("verified: true");

    Ok(())
}

pub(crate) fn run_zh_artifact_inspect(
    options: ZhArtifactInspectCliOptions,
) -> Result<(), Box<dyn Error>> {
    let index = load_zh_artifact_payload_by_format(
        Path::new(&options.payload),
        options.payload_format.as_str(),
    )?;

    println!("payload: {}", options.payload);
    println!("format: {}", options.payload_format.as_str());
    println!("pinyin_view: {}", index.pinyin_view().as_str());
    println!("entries: {}", index.len());
    println!("checksum_algorithm: {ZH_ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM}");
    println!("checksum: {}", index.artifact_payload_checksum());

    Ok(())
}

pub(crate) fn run_zh_artifact_bundle(
    options: ZhArtifactBundleCliOptions,
) -> Result<(), Box<dyn Error>> {
    let index =
        ZhReadingIndex::from_cedict_path_with_options(&options.cedict, options.index_options)?;
    let output_dir = PathBuf::from(&options.output_dir);
    let payload_file_name =
        default_zh_payload_file_name(&options.artifact_name, options.payload_format);
    let payload_path = output_dir.join(&payload_file_name);
    let metadata_path = output_dir.join("metadata.yaml");
    let license_output_dir = output_dir.join("license");

    fs::create_dir_all(&license_output_dir)?;
    write_zh_artifact_payload_file(&index, options.payload_format, &payload_path)?;
    let file_digest = zh_artifact_file_digest_path(&payload_path)?;

    let mut metadata = index.artifact_metadata(ZhArtifactMetadataOptions {
        artifact_name: options.artifact_name,
        generator: "moine-cli".to_string(),
        payload_file_name,
        payload_format: options.payload_format.as_str().to_string(),
        source_name: options.source_name,
        source_version: options.source_version,
        source_cedict: options.cedict.clone(),
        index_options: options.index_options,
        query_defaults: options.reading_options,
        license: ZhArtifactLicense::default(),
    });
    metadata.payload.file_digest_algorithm =
        Some(ZH_ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM.to_string());
    metadata.payload.file_digest = Some(file_digest.clone());
    fs::write(&metadata_path, serde_yaml::to_string(&metadata)?)?;

    write_zh_license_reference(options.license_file.as_deref(), &license_output_dir)?;

    println!("bundle: {}", output_dir.display());
    println!("metadata: {}", metadata_path.display());
    println!("payload: {}", payload_path.display());
    println!("payload_format: {}", options.payload_format.as_str());
    println!("pinyin_view: {}", index.pinyin_view().as_str());
    println!("entries: {}", index.len());
    println!("file_digest_algorithm: {ZH_ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM}");
    println!("file_digest: {file_digest}");
    println!("checksum: {}", index.artifact_payload_checksum());

    Ok(())
}

pub(crate) fn run_zh_artifact_verify(
    options: ZhArtifactVerifyCliOptions,
) -> Result<(), Box<dyn Error>> {
    let verified = verify_zh_artifact_bundle(&options.metadata, options.bundle_dir.as_deref())?;

    println!("metadata: {}", verified.metadata_path.display());
    println!("bundle: {}", verified.bundle_dir.display());
    println!("payload: {}", verified.payload_path.display());
    println!("pinyin_view: {}", verified.index.pinyin_view().as_str());
    println!("entries: {}", verified.index.len());
    if let Some(file_digest) = &verified.file_digest {
        println!("file_digest_algorithm: {ZH_ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM}");
        println!("file_digest: {file_digest}");
    } else {
        println!("file_digest: skipped");
    }
    println!(
        "checksum_algorithm: {}",
        verified.metadata.payload.checksum_algorithm
    );
    println!("checksum: {}", verified.checksum);
    println!(
        "license_references: {}",
        verified.metadata.license.references.len()
    );
    println!("verified: true");

    Ok(())
}

pub(crate) struct VerifiedZhArtifactBundle {
    pub(crate) metadata_path: PathBuf,
    pub(crate) bundle_dir: PathBuf,
    pub(crate) payload_path: PathBuf,
    pub(crate) metadata: ZhArtifactMetadata,
    pub(crate) index: ZhReadingIndex,
    pub(crate) file_digest: Option<String>,
    pub(crate) checksum: String,
}

pub(crate) fn write_zh_license_reference(
    license_file: Option<&str>,
    output_license_dir: &Path,
) -> Result<(), std::io::Error> {
    let output = output_license_dir.join("CC-CEDICT.md");
    if let Some(license_file) = license_file {
        fs::copy(license_file, output)?;
    } else {
        fs::write(
            output,
            "CC-CEDICT dictionary data is distributed under CC BY-SA 4.0.\n\
This moine artifact is derived from CC-CEDICT and keeps its license separate \
from the moine source-code license.\n",
        )?;
    }
    Ok(())
}

pub(crate) fn write_zh_artifact_payload_file(
    index: &ZhReadingIndex,
    payload_format: ArtifactPayloadFormat,
    path: &Path,
) -> Result<(), Box<dyn Error>> {
    match payload_format {
        ArtifactPayloadFormat::Yaml => {
            write_output_file(path, serde_yaml::to_string(&index.artifact_payload())?)?;
        }
        ArtifactPayloadFormat::Indexed => {
            let output = create_output_file(path)?;
            index.write_indexed_artifact_payload(output)?;
        }
        ArtifactPayloadFormat::Binary => {
            return Err(Box::new(CliError::ArtifactVerificationFailed(
                "binary zh payload format is not implemented; use yaml or indexed".to_string(),
            )));
        }
    }
    Ok(())
}

pub(crate) fn load_zh_index(
    source: &ZhIndexSource,
    index_options: CedictIndexOptions,
) -> Result<ZhReadingIndex, Box<dyn Error>> {
    match source {
        ZhIndexSource::Cedict(path) => Ok(ZhReadingIndex::from_cedict_path_with_options(
            path,
            index_options,
        )?),
        ZhIndexSource::ArtifactPayload {
            path,
            payload_format,
        } => load_zh_artifact_payload_by_format(Path::new(path), payload_format.as_str()),
        ZhIndexSource::ArtifactMetadata(path) => Ok(verify_zh_artifact_bundle(path, None)?.index),
    }
}

pub(crate) fn verify_zh_artifact_bundle(
    metadata: &str,
    bundle_dir: Option<&str>,
) -> Result<VerifiedZhArtifactBundle, Box<dyn Error>> {
    let metadata_path = PathBuf::from(metadata);
    let metadata_yaml = fs::read_to_string(&metadata_path)?;
    let metadata = serde_yaml::from_str::<ZhArtifactMetadata>(&metadata_yaml)?;
    if metadata.schema_version != 1 {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "unsupported zh metadata schema version {}",
            metadata.schema_version
        ))));
    }
    if metadata.artifact_type != "moine.zh.reading-index" {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "unsupported zh artifact type {:?}",
            metadata.artifact_type
        ))));
    }
    let bundle_dir = bundle_dir.map(PathBuf::from).unwrap_or_else(|| {
        metadata_path
            .parent()
            .unwrap_or(Path::new("."))
            .to_path_buf()
    });
    let payload_path = checked_bundle_path(&bundle_dir, &metadata.payload.path)?;
    let file_digest = verify_zh_payload_file_digest(&metadata, &payload_path)?;
    let index = load_zh_artifact_payload_by_format(&payload_path, &metadata.payload.format)?;
    if index.len() != metadata.build.entries {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "entry count mismatch: metadata has {}, payload has {}",
            metadata.build.entries,
            index.len()
        ))));
    }
    if index.pinyin_view().as_str() != metadata.build.pinyin_view {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "pinyin view mismatch: metadata has {}, payload has {}",
            metadata.build.pinyin_view,
            index.pinyin_view().as_str()
        ))));
    }
    let checksum = index
        .artifact_payload_checksum_for_algorithm(&metadata.payload.checksum_algorithm)
        .ok_or_else(|| {
            CliError::ArtifactVerificationFailed(format!(
                "unsupported checksum algorithm {:?}",
                metadata.payload.checksum_algorithm
            ))
        })?;
    if checksum != metadata.payload.checksum {
        return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "payload checksum mismatch: metadata has {}, recomputed {}",
            metadata.payload.checksum, checksum
        ))));
    }
    for reference in &metadata.license.references {
        let path = checked_bundle_path(&bundle_dir, &reference.path)?;
        if !path.is_file() {
            return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                "missing license reference {} at {}",
                reference.label,
                path.display()
            ))));
        }
    }

    Ok(VerifiedZhArtifactBundle {
        metadata_path,
        bundle_dir,
        payload_path,
        metadata,
        index,
        file_digest,
        checksum,
    })
}

pub(crate) fn verify_zh_payload_file_digest(
    metadata: &ZhArtifactMetadata,
    payload_path: &Path,
) -> Result<Option<String>, Box<dyn Error>> {
    match (
        metadata.payload.file_digest_algorithm.as_deref(),
        metadata.payload.file_digest.as_deref(),
    ) {
        (None, None) => Ok(None),
        (Some(algorithm), Some(expected)) => {
            if algorithm != ZH_ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM {
                return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                    "unsupported zh payload file digest algorithm {algorithm:?}"
                ))));
            }
            let digest = zh_artifact_file_digest_path(payload_path)?;
            if digest != expected {
                return Err(Box::new(CliError::ArtifactVerificationFailed(format!(
                    "payload file digest mismatch: metadata has {expected}, recomputed {digest}"
                ))));
            }
            Ok(Some(digest))
        }
        _ => Err(Box::new(CliError::ArtifactVerificationFailed(
            "zh payload file digest algorithm and digest must be provided together".to_string(),
        ))),
    }
}

pub(crate) fn load_zh_artifact_payload_by_format(
    path: &Path,
    payload_format: &str,
) -> Result<ZhReadingIndex, Box<dyn Error>> {
    match payload_format {
        YAML_PAYLOAD_FORMAT => Ok(ZhReadingIndex::from_artifact_payload_path(path)?),
        INDEXED_PAYLOAD_FORMAT => Ok(ZhReadingIndex::from_indexed_artifact_payload_path(path)?),
        _ => Err(Box::new(CliError::ArtifactVerificationFailed(format!(
            "unsupported zh payload format {payload_format:?}"
        )))),
    }
}

pub(crate) fn zh_release_archive_entries(
    verified: &VerifiedZhArtifactBundle,
) -> Result<Vec<ArchiveEntry>, Box<dyn Error>> {
    let mut entries = vec![
        ArchiveEntry {
            source: verified.metadata_path.clone(),
            path: "metadata.yaml".to_string(),
        },
        ArchiveEntry {
            source: verified.payload_path.clone(),
            path: normalized_relative_archive_path(&verified.metadata.payload.path)?,
        },
    ];
    for reference in &verified.metadata.license.references {
        entries.push(ArchiveEntry {
            source: checked_bundle_path(&verified.bundle_dir, &reference.path)?,
            path: normalized_relative_archive_path(&reference.path)?,
        });
    }
    entries.sort_by(|left, right| left.path.cmp(&right.path));
    entries.dedup_by(|left, right| left.path == right.path);
    Ok(entries)
}

pub(crate) fn run_zh_artifact_release_checksums(
    options: ZhArtifactReleaseChecksumsCliOptions,
) -> Result<(), Box<dyn Error>> {
    let mut output = String::new();

    for asset in &options.assets {
        let path = Path::new(asset);
        let digest = zh_artifact_file_digest_path(path)?;
        let label = release_checksum_asset_label(path)?;
        writeln!(output, "{digest}  {label}")?;
    }

    if let Some(path) = &options.output {
        write_output_file(path, output)?;
    } else {
        print!("{output}");
    }

    Ok(())
}
