use std::io::BufWriter;
use std::path::Path;

use serde::Serialize;
use serde::ser::SerializeSeq;
use sha2::Digest;

use crate::audit::{
    AspaOutput, AuditRunMeta, AuditWarning, QueryAuditManifest, ValidationEvent,
    ValidationEventCounts, VrpOutput,
};
use crate::ccr::canonical_vrp_prefix;

use super::{PostValidationShared, RunStageTiming};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ReportJsonFormat {
    Pretty,
    Compact,
}

pub(super) fn write_json<T: Serialize>(
    path: &Path,
    report: &T,
    format: ReportJsonFormat,
) -> Result<(), String> {
    let f = std::fs::File::create(path)
        .map_err(|e| format!("create report file failed: {}: {e}", path.display()))?;
    let writer = BufWriter::new(f);
    match format {
        ReportJsonFormat::Pretty => serde_json::to_writer_pretty(writer, report),
        ReportJsonFormat::Compact => serde_json::to_writer(writer, report),
    }
    .map_err(|e| format!("write report json failed: {e}"))?;
    Ok(())
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ReportJsonWriteTiming {
    pub(super) build_ms: u64,
    pub(super) write_ms: u64,
}

#[derive(Serialize)]
struct BorrowedAuditReportV2<'a> {
    format_version: u32,
    meta: AuditRunMeta,
    policy: &'a crate::policy::Policy,
    tree: BorrowedTreeSummary<'a>,
    publication_points: &'a [crate::audit::PublicationPointAudit],
    vrps: VrpReportSequence<'a>,
    aspas: AspaReportSequence<'a>,
    downloads: &'a [crate::audit::AuditDownloadEvent],
    download_stats: &'a crate::audit::AuditDownloadStats,
    repo_sync_stats: crate::audit::AuditRepoSyncStats,
    #[serde(rename = "queryAudit", skip_serializing_if = "Option::is_none")]
    query_audit: Option<QueryAuditManifest>,
}

#[derive(Serialize)]
struct BorrowedTreeSummary<'a> {
    instances_processed: usize,
    instances_failed: usize,
    warnings: WarningReportSequence<'a>,
}

struct WarningReportSequence<'a>(&'a [crate::report::Warning]);

impl Serialize for WarningReportSequence<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
        for warning in self.0 {
            seq.serialize_element(&AuditWarning::from(warning))?;
        }
        seq.end()
    }
}

struct VrpReportSequence<'a>(&'a [crate::validation::objects::Vrp]);

impl Serialize for VrpReportSequence<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
        for vrp in self.0 {
            seq.serialize_element(&VrpOutput {
                asn: vrp.asn,
                prefix: crate::audit::format_roa_ip_prefix(&vrp.prefix),
                max_length: vrp.max_length,
            })?;
        }
        seq.end()
    }
}

struct AspaReportSequence<'a>(&'a [crate::validation::objects::AspaAttestation]);

impl Serialize for AspaReportSequence<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
        for aspa in self.0 {
            seq.serialize_element(&AspaOutput {
                customer_as_id: aspa.customer_as_id,
                provider_as_ids: aspa.provider_as_ids.clone(),
            })?;
        }
        seq.end()
    }
}

pub(super) fn write_report_json_from_shared(
    path: &Path,
    policy: &crate::policy::Policy,
    validation_time: time::OffsetDateTime,
    shared: &PostValidationShared,
    format: ReportJsonFormat,
) -> Result<ReportJsonWriteTiming, String> {
    use time::format_description::well_known::Rfc3339;

    let build_started = std::time::Instant::now();
    let validation_time_rfc3339_utc = validation_time
        .to_offset(time::UtcOffset::UTC)
        .format(&Rfc3339)
        .expect("format validation_time");
    let repo_sync_stats = super::build_repo_sync_stats(shared.publication_points.as_ref());
    let query_audit = write_validation_events_sidecar(path, &validation_time_rfc3339_utc, shared)?;
    let report = BorrowedAuditReportV2 {
        format_version: 2,
        meta: AuditRunMeta {
            validation_time_rfc3339_utc,
        },
        policy,
        tree: BorrowedTreeSummary {
            instances_processed: shared.instances_processed,
            instances_failed: shared.instances_failed,
            warnings: WarningReportSequence(shared.tree_warnings.as_ref()),
        },
        publication_points: shared.publication_points.as_ref(),
        vrps: VrpReportSequence(shared.vrps.as_ref()),
        aspas: AspaReportSequence(shared.aspas.as_ref()),
        downloads: shared.downloads.as_ref(),
        download_stats: &shared.download_stats,
        repo_sync_stats,
        query_audit: Some(query_audit),
    };
    let build_ms = build_started.elapsed().as_millis() as u64;

    let write_started = std::time::Instant::now();
    write_json(path, &report, format)?;
    Ok(ReportJsonWriteTiming {
        build_ms,
        write_ms: write_started.elapsed().as_millis() as u64,
    })
}

fn write_validation_events_sidecar(
    report_path: &Path,
    validation_time: &str,
    shared: &PostValidationShared,
) -> Result<QueryAuditManifest, String> {
    let events_path = report_path.with_file_name("validation-events.jsonl");
    if let Some(parent) = events_path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("create validation events parent failed: {e}"))?;
    }
    let mut writer = BufWriter::new(std::fs::File::create(&events_path).map_err(|e| {
        format!(
            "create validation events failed: {}: {e}",
            events_path.display()
        )
    })?);
    let mut seq = 0u64;
    let mut hasher = sha2::Sha256::new();
    emit_validation_events(validation_time, shared, &mut seq, &mut |event| {
        let mut line = serde_json::to_vec(&event)
            .map_err(|e| format!("serialize validation event failed: {e}"))?;
        line.push(b'\n');
        std::io::Write::write_all(&mut writer, &line)
            .map_err(|e| format!("write validation event failed: {e}"))?;
        hasher.update(&line);
        Ok(())
    })?;
    std::io::Write::flush(&mut writer)
        .map_err(|e| format!("flush validation events failed: {e}"))?;
    let events_count = seq;
    let events_sha256 = hex::encode(hasher.finalize());
    Ok(QueryAuditManifest {
        schema_version: 1,
        status: "complete".to_string(),
        events_path: events_path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("validation-events.jsonl")
            .to_string(),
        events_count,
        events_sha256,
        writer_version: 1,
        error: None,
    })
}

fn emit_validation_events(
    validation_time: &str,
    shared: &PostValidationShared,
    seq: &mut u64,
    emit: &mut impl FnMut(ValidationEvent) -> Result<(), String>,
) -> Result<(), String> {
    emit(next_event(seq, "run_summary", validation_time, |event| {
        event.counts = Some(ValidationEventCounts {
            objects: Some(
                shared
                    .publication_points
                    .iter()
                    .map(|pp| pp.objects.len() as u64)
                    .sum(),
            ),
            warnings: Some(
                (shared.tree_warnings.len()
                    + shared
                        .publication_points
                        .iter()
                        .map(|pp| pp.warnings.len())
                        .sum::<usize>()) as u64,
            ),
            vrps: Some(shared.vrps.len() as u64),
            aspas: Some(shared.aspas.len() as u64),
        });
    }))?;
    for pp in shared.publication_points.iter() {
        emit(next_event(
            seq,
            "publication_point",
            validation_time,
            |event| {
                event.pp_node_id = pp.node_id;
                event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
                event.pp_rsync_base_uri = Some(pp.rsync_base_uri.clone());
                event.repo_sync_phase = pp.repo_sync_phase.clone();
                event.repo_terminal_state = Some(pp.repo_terminal_state.clone());
                event.counts = Some(ValidationEventCounts {
                    objects: Some(pp.objects.len() as u64),
                    warnings: Some(pp.warnings.len() as u64),
                    vrps: None,
                    aspas: None,
                });
            },
        ))?;
        for object in &pp.objects {
            emit(next_event(seq, "object", validation_time, |event| {
                event.pp_node_id = pp.node_id;
                event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
                event.object_uri = Some(object.rsync_uri.clone());
                event.sha256 = Some(object.sha256_hex.clone());
                event.object_type = Some(object.kind.clone());
                event.result = Some(object.result.clone());
                event.reason = object.detail.clone();
            }))?;
        }
        for warning in &pp.warnings {
            emit(next_event(seq, "warning", validation_time, |event| {
                event.pp_node_id = pp.node_id;
                event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
                event.reason = Some(warning.message.clone());
            }))?;
        }
    }
    Ok(())
}

fn next_event(
    seq: &mut u64,
    event_type: &str,
    validation_time: &str,
    fill: impl FnOnce(&mut ValidationEvent),
) -> ValidationEvent {
    *seq += 1;
    let mut event = ValidationEvent {
        schema_version: 1,
        seq: *seq,
        event_type: event_type.to_string(),
        validation_time: validation_time.to_string(),
        pp_node_id: None,
        pp_manifest_uri: None,
        pp_rsync_base_uri: None,
        repo_sync_phase: None,
        repo_terminal_state: None,
        object_uri: None,
        sha256: None,
        object_type: None,
        result: None,
        reason: None,
        counts: None,
    };
    fill(&mut event);
    event
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct CompareViewTaskOutput {
    pub(super) build_ms: Option<u64>,
    pub(super) write_ms: Option<u64>,
}

pub(super) fn run_compare_view_task(
    shared: &PostValidationShared,
    vrps_csv_out_path: Option<&Path>,
    vaps_csv_out_path: Option<&Path>,
    trust_anchor: &str,
) -> Result<CompareViewTaskOutput, String> {
    let mut build_ms = None;
    let mut write_ms = None;
    if let (Some(vrps_path), Some(vaps_path)) = (vrps_csv_out_path, vaps_csv_out_path) {
        let started = std::time::Instant::now();
        build_ms = Some(0);
        write_direct_vrp_csv(vrps_path, shared.vrps.as_ref(), trust_anchor)?;
        write_direct_vap_csv(vaps_path, shared.aspas.as_ref(), trust_anchor)?;
        write_ms = Some(started.elapsed().as_millis() as u64);
        eprintln!(
            "wrote compare views: vrps={} vaps={}",
            vrps_path.display(),
            vaps_path.display()
        );
    }
    Ok(CompareViewTaskOutput { build_ms, write_ms })
}

fn write_direct_vrp_csv(
    path: &Path,
    vrps: &[crate::validation::objects::Vrp],
    trust_anchor: &str,
) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
    }
    let file = std::fs::File::create(path)
        .map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
    let mut writer = BufWriter::new(file);
    use std::io::Write;
    let trust_anchor = trust_anchor.to_ascii_lowercase();
    writeln!(writer, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?;
    for vrp in vrps {
        writeln!(
            writer,
            "AS{},{},{},{}",
            vrp.asn,
            canonical_vrp_prefix(&vrp.prefix),
            vrp.max_length,
            trust_anchor
        )
        .map_err(|e| e.to_string())?;
    }
    Ok(())
}

fn write_direct_vap_csv(
    path: &Path,
    aspas: &[crate::validation::objects::AspaAttestation],
    trust_anchor: &str,
) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
    }
    let file = std::fs::File::create(path)
        .map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
    let mut writer = BufWriter::new(file);
    use std::io::Write;
    let trust_anchor = trust_anchor.to_ascii_lowercase();
    writeln!(writer, "Customer ASN,Providers,Trust Anchor").map_err(|e| e.to_string())?;
    for aspa in aspas {
        let mut providers = aspa.provider_as_ids.clone();
        providers.sort_unstable();
        providers.dedup();
        let providers = providers
            .into_iter()
            .map(|asn| format!("AS{asn}"))
            .collect::<Vec<_>>()
            .join(";");
        writeln!(
            writer,
            "AS{},{},{}",
            aspa.customer_as_id, providers, trust_anchor
        )
        .map_err(|e| e.to_string())?;
    }
    Ok(())
}

pub(super) fn write_stage_timing(
    report_json_path: Option<&Path>,
    stage_timing: &RunStageTiming,
) -> Result<(), String> {
    if let Some(path) = report_json_path
        && let Some(parent) = path.parent()
    {
        let stage_timing_path = parent.join("stage-timing.json");
        std::fs::write(
            &stage_timing_path,
            serde_json::to_vec_pretty(stage_timing).map_err(|e| e.to_string())?,
        )
        .map_err(|e| {
            format!(
                "write stage timing failed: {}: {e}",
                stage_timing_path.display()
            )
        })?;
        eprintln!("analysis: wrote {}", stage_timing_path.display());
    }
    Ok(())
}
