use chrono::{DateTime, Utc};
use clap::ValueEnum;
use serde::{Deserialize, Serialize};

pub type SchemaVersion = u32;
pub const SCHEMA_VERSION: SchemaVersion = 1;

#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
    Text,
    Json,
    Markdown,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
    pub name: String,
    pub version: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CheckStatus {
    Pass,
    Warn,
    Fail,
    Skip,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Check {
    pub id: String,
    pub status: CheckStatus,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub evidence: Option<CheckEvidence>,
}

impl Check {
    pub fn pass(id: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(id, CheckStatus::Pass, message)
    }

    pub fn warn(id: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(id, CheckStatus::Warn, message)
    }

    pub fn fail(id: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(id, CheckStatus::Fail, message)
    }

    pub fn skip(id: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(id, CheckStatus::Skip, message)
    }

    fn new(id: impl Into<String>, status: CheckStatus, message: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            status,
            message: message.into(),
            location: None,
            evidence: None,
        }
    }

    pub fn at(mut self, location: impl Into<String>) -> Self {
        self.location = Some(location.into());
        self
    }

    pub fn with_evidence(mut self, evidence: CheckEvidence) -> Self {
        self.evidence = Some(evidence);
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CheckEvidence {
    pub kind: EvidenceKind,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exit_code: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stdout: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stderr: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subject: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum EvidenceKind {
    HostCommand,
    DockerInspect,
    DockerRun,
    ManifestField,
    ToolcacheLookup,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReceiptSummary {
    pub passed: usize,
    pub warnings: usize,
    pub failed: usize,
    pub skipped: usize,
}

impl ReceiptSummary {
    pub fn from_checks(checks: &[Check]) -> Self {
        let mut summary = Self::default();
        for check in checks {
            match check.status {
                CheckStatus::Pass => summary.passed += 1,
                CheckStatus::Warn => summary.warnings += 1,
                CheckStatus::Fail => summary.failed += 1,
                CheckStatus::Skip => summary.skipped += 1,
            }
        }
        summary
    }

    pub fn add(&mut self, other: &Self) {
        self.passed += other.passed;
        self.warnings += other.warnings;
        self.failed += other.failed;
        self.skipped += other.skipped;
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Compatibility {
    Exact,
    Simulated,
    Unsupported,
}

impl Compatibility {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Exact => "exact",
            Self::Simulated => "simulated",
            Self::Unsupported => "unsupported",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckReport {
    pub manifest_path: String,
    pub schema_version: SchemaVersion,
    pub image_name: String,
    pub summary: ReceiptSummary,
    pub checks: Vec<Check>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeHostReport {
    pub manifest_path: String,
    pub image_name: String,
    pub tools: Vec<ToolProbeOutcome>,
    pub packages: Vec<PackageProbeOutcome>,
    pub capabilities: Vec<CapabilityProbeOutcome>,
    pub summary: ReceiptSummary,
    pub checks: Vec<Check>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeDockerReport {
    pub manifest_path: String,
    pub image_name: String,
    pub requested_image: String,
    pub docker_cli_available: bool,
    pub inspect: Option<DockerInspectEvidence>,
    pub tools: Vec<ToolProbeOutcome>,
    pub packages: Vec<PackageProbeOutcome>,
    pub capabilities: Vec<CapabilityProbeOutcome>,
    pub summary: ReceiptSummary,
    pub checks: Vec<Check>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerInspectEvidence {
    pub repo_tags: Vec<String>,
    pub digest: Option<String>,
    pub size: Option<i64>,
    pub created: Option<String>,
    pub os: Option<String>,
    pub architecture: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolProbeOutcome {
    pub name: String,
    pub expected_version: Option<String>,
    pub observed_version: Option<String>,
    pub classification: Compatibility,
    pub status: CheckStatus,
    pub reason: Option<String>,
    pub command: Option<String>,
    pub exit_code: Option<i32>,
    pub stdout_excerpt: Option<String>,
    pub stderr_excerpt: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageProbeOutcome {
    pub name: String,
    pub manager: String,
    pub expected_version: Option<String>,
    pub observed_version: Option<String>,
    pub classification: Compatibility,
    pub status: CheckStatus,
    pub reason: Option<String>,
    pub command: Option<String>,
    pub exit_code: Option<i32>,
    pub stdout_excerpt: Option<String>,
    pub stderr_excerpt: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityProbeOutcome {
    pub name: String,
    pub classification: Compatibility,
    pub status: CheckStatus,
    pub reason: Option<String>,
    pub command: Option<String>,
    pub exit_code: Option<i32>,
    pub stdout_excerpt: Option<String>,
    pub stderr_excerpt: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupNodeReport {
    pub manifest_path: String,
    pub image_name: String,
    pub requested_version: String,
    pub resolved_version: Option<String>,
    pub arch: String,
    pub toolcache_path: Option<String>,
    pub path_prepend: Option<String>,
    pub classification: Compatibility,
    pub reason: Option<String>,
    pub check_latest: bool,
    pub summary: ReceiptSummary,
    pub checks: Vec<Check>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReceiptMode {
    Check,
    ProbeHost,
    ProbeDocker,
    SetupNode,
}

impl ReceiptMode {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Check => "check",
            Self::ProbeHost => "probe-host",
            Self::ProbeDocker => "probe-docker",
            Self::SetupNode => "setup-node",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnerImageReceipt {
    pub schema_version: SchemaVersion,
    pub tool: ToolInfo,
    pub checked_at: DateTime<Utc>,
    pub mode: ReceiptMode,
    pub summary: ReceiptSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub check: Option<CheckReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub probe_host: Option<ProbeHostReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub probe_docker: Option<ProbeDockerReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub setup_node: Option<SetupNodeReport>,
    pub checks: Vec<Check>,
}
