use super::{
    DataShape, PackageDirectoryLayout, PackageManifest, PackageMetadata,
    PackageValidationIssueCode, PackageValidationReport, RuntimeBackend, SchemaVersion,
};
use std::collections::HashMap;

/// Validate package manifest fields that do not require filesystem access.
pub fn validate_package_manifest(manifest: &PackageManifest) -> PackageValidationReport {
    let mut report = PackageValidationReport::default();

    push_required_issue(&mut report, "name", &manifest.name);
    validate_v1_contract(&mut report, manifest);
    push_required_issue(&mut report, "model.path", &manifest.model.path);
    if let Some(metadata) = &manifest.model.metadata {
        push_required_issue(&mut report, "model.metadata.name", &metadata.name);
    }
    validate_contract_identifiers(&mut report, manifest);
    validate_runtime_contract(&mut report, manifest);
    validate_fixture_list(&mut report, manifest);
    for (field, shape) in [
        ("expected_input", manifest.expected_input.as_ref()),
        ("expected_output", manifest.expected_output.as_ref()),
    ] {
        if let Some(shape) = shape {
            validate_shape(&mut report, field, shape);
        }
    }

    report.finish()
}

fn validate_contract_identifiers(report: &mut PackageValidationReport, manifest: &PackageManifest) {
    if let Some(tokenizer) = &manifest.tokenizer {
        validate_token_asset_identifiers(report, "tokenizer", tokenizer);
    }
    if let Some(vocab) = &manifest.vocab {
        validate_token_asset_identifiers(report, "vocab", vocab);
    }
    validate_pipeline_step_identifiers(report, "preprocessing", &manifest.preprocessing);
    validate_pipeline_step_identifiers(report, "postprocessing", &manifest.postprocessing);
}

fn validate_token_asset_identifiers(
    report: &mut PackageValidationReport,
    field: &str,
    asset: &super::TokenAsset,
) {
    push_required_issue(report, &format!("{field}.name"), &asset.name);
    if let Some(contract_version) = &asset.contract_version {
        push_required_issue(
            report,
            &format!("{field}.contract_version"),
            contract_version,
        );
    }
}

fn validate_pipeline_step_identifiers(
    report: &mut PackageValidationReport,
    section: &str,
    steps: &[super::PipelineStep],
) {
    for (index, step) in steps.iter().enumerate() {
        push_required_issue(report, &format!("{section}[{index}].name"), &step.name);
        push_required_issue(
            report,
            &format!("{section}[{index}].implementation"),
            &step.implementation,
        );
        push_required_issue(
            report,
            &format!("{section}[{index}].contract"),
            &step.contract,
        );
        if let Some(contract_version) = &step.contract_version {
            push_required_issue(
                report,
                &format!("{section}[{index}].contract_version"),
                contract_version,
            );
        }
    }
}

fn validate_runtime_contract(report: &mut PackageValidationReport, manifest: &PackageManifest) {
    if manifest.runtime.backend == RuntimeBackend::ExternalProcess {
        report.push_issue(
            PackageValidationIssueCode::UnsupportedRuntimeBackend,
            "runtime.backend",
            "runtime.backend 'external-process' is experimental and is not supported by the public package manifest contract",
        );
    }
}

fn validate_v1_contract(report: &mut PackageValidationReport, manifest: &PackageManifest) {
    if manifest.schema_version != SchemaVersion::BiorsPackageV1 {
        if let Some(layout) = &manifest.package_layout {
            validate_package_layout_fields(report, layout, manifest);
        }
        if let Some(metadata) = &manifest.metadata {
            validate_metadata_fields(report, metadata);
        }
        return;
    }

    match &manifest.package_layout {
        Some(layout) => validate_package_layout_fields(report, layout, manifest),
        None => report.push_issue(
            PackageValidationIssueCode::RequiredField,
            "package_layout",
            "package_layout is required for biors.package.v1",
        ),
    }

    match &manifest.metadata {
        Some(metadata) => validate_metadata_fields(report, metadata),
        None => report.push_issue(
            PackageValidationIssueCode::RequiredField,
            "metadata",
            "metadata is required for biors.package.v1",
        ),
    }
}

fn validate_package_layout_fields(
    report: &mut PackageValidationReport,
    layout: &PackageDirectoryLayout,
    manifest: &PackageManifest,
) {
    push_required_issue(report, "package_layout.manifest", &layout.manifest);
    push_required_issue(report, "package_layout.models", &layout.models);
    push_required_issue(report, "package_layout.fixtures", &layout.fixtures);
    push_required_issue(report, "package_layout.docs", &layout.docs);

    if manifest.tokenizer.is_some() {
        push_required_option_issue(
            report,
            "package_layout.tokenizers",
            layout.tokenizers.as_ref(),
        );
    }
    if manifest.vocab.is_some() {
        push_required_option_issue(report, "package_layout.vocabs", layout.vocabs.as_ref());
    }
    if pipeline_steps_have_config(manifest) {
        push_required_option_issue(
            report,
            "package_layout.pipelines",
            layout.pipelines.as_ref(),
        );
    }
    validate_pipeline_step_configs(report, "preprocessing", &manifest.preprocessing);
    validate_pipeline_step_configs(report, "postprocessing", &manifest.postprocessing);
}

fn validate_metadata_fields(report: &mut PackageValidationReport, metadata: &PackageMetadata) {
    push_required_issue(
        report,
        "metadata.license.expression",
        &metadata.license.expression,
    );
    push_required_issue(
        report,
        "metadata.citation.preferred_citation",
        &metadata.citation.preferred_citation,
    );
    push_required_issue(
        report,
        "metadata.model_card.path",
        &metadata.model_card.path,
    );
    push_required_issue(
        report,
        "metadata.model_card.summary",
        &metadata.model_card.summary,
    );
    push_required_list_issue(
        report,
        "metadata.model_card.intended_use",
        &metadata.model_card.intended_use,
    );
    push_required_list_issue(
        report,
        "metadata.model_card.limitations",
        &metadata.model_card.limitations,
    );
}

fn validate_fixture_list(report: &mut PackageValidationReport, manifest: &PackageManifest) {
    if manifest.fixtures.is_empty() {
        report.push_issue(
            PackageValidationIssueCode::MissingFixture,
            "fixtures",
            "fixtures must include at least one fixture",
        );
        return;
    }

    for (index, fixture) in manifest.fixtures.iter().enumerate() {
        push_required_issue(report, &format!("fixtures[{index}].name"), &fixture.name);
        push_required_issue(report, &format!("fixtures[{index}].input"), &fixture.input);
        push_required_issue(
            report,
            &format!("fixtures[{index}].expected_output"),
            &fixture.expected_output,
        );
    }
    reject_duplicate_fixture_names(report, manifest);
}

fn reject_duplicate_fixture_names(
    report: &mut PackageValidationReport,
    manifest: &PackageManifest,
) {
    let mut first_index_by_name: HashMap<&str, usize> = HashMap::new();
    for (index, fixture) in manifest.fixtures.iter().enumerate() {
        let name = fixture.name.trim();
        if name.is_empty() {
            continue;
        }
        match first_index_by_name.get(name) {
            Some(first_index) => report.push_issue(
                PackageValidationIssueCode::DuplicateFixtureName,
                &format!("fixtures[{index}].name"),
                &format!("fixture name '{name}' duplicates fixtures[{first_index}].name"),
            ),
            None => {
                first_index_by_name.insert(name, index);
            }
        }
    }
}

fn validate_pipeline_step_configs(
    report: &mut PackageValidationReport,
    section: &str,
    steps: &[super::PipelineStep],
) {
    for (index, step) in steps.iter().enumerate() {
        if let Some(config) = &step.config {
            push_required_issue(
                report,
                &format!("{section}[{index}].config.path"),
                &config.path,
            );
        }
    }
}

fn pipeline_steps_have_config(manifest: &PackageManifest) -> bool {
    manifest
        .preprocessing
        .iter()
        .chain(manifest.postprocessing.iter())
        .any(|step| step.config.is_some())
}

fn push_required_issue(report: &mut PackageValidationReport, field: &str, value: &str) {
    if value.trim().is_empty() {
        report.push_issue(
            PackageValidationIssueCode::RequiredField,
            field,
            &format!("{field} is required"),
        );
    }
}

fn push_required_option_issue(
    report: &mut PackageValidationReport,
    field: &str,
    value: Option<&String>,
) {
    match value {
        Some(value) => push_required_issue(report, field, value),
        None => report.push_issue(
            PackageValidationIssueCode::RequiredField,
            field,
            &format!("{field} is required"),
        ),
    }
}

fn push_required_list_issue(report: &mut PackageValidationReport, field: &str, value: &[String]) {
    if value.is_empty() || value.iter().any(|entry| entry.trim().is_empty()) {
        report.push_issue(
            PackageValidationIssueCode::RequiredField,
            field,
            &format!("{field} must include non-empty values"),
        );
    }
}

fn validate_shape(report: &mut PackageValidationReport, field: &str, shape: &DataShape) {
    if shape.shape.is_empty() {
        report.push_issue(
            PackageValidationIssueCode::InvalidShape,
            &format!("{field}.shape"),
            &format!("{field}.shape must include at least one dimension"),
        );
    }
    for (index, dimension) in shape.shape.iter().enumerate() {
        if dimension.trim().is_empty() {
            let field = format!("{field}.shape[{index}]");
            report.push_issue(
                PackageValidationIssueCode::InvalidShape,
                &field,
                &format!("{field} must be a non-empty dimension"),
            );
        }
    }
}
