use std::{collections::BTreeMap, sync::Arc};

use serde::Serialize;
use turbopath::{AnchoredSystemPathBuf, RelativeUnixPathBuf};
use turborepo_cache::CacheHitMetadata;
use turborepo_env::{DetailedMap, EnvironmentVariableMap};
use turborepo_task_id::TaskId;
use turborepo_types::{
    EnvMode, HashTrackerCacheHitMetadata, HashTrackerDetailedMap, OutputLogsMode, TaskDefinition,
    TaskOutputs,
};

use crate::execution::TaskSummaryInfo;

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TaskExecutionSummary {
    pub start_time: i64,
    pub end_time: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub exit_code: Option<i32>,
}

impl TaskExecutionSummary {
    pub fn is_failure(&self) -> bool {
        // We consider None as a failure as it indicates the task failed to start
        // or was killed in a manner where we didn't collect an exit code.
        !matches!(self.exit_code, Some(0))
    }
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TaskCacheSummary {
    // Deprecated, but keeping around for --dry=json
    pub local: bool,
    // Deprecated, but keeping around for --dry=json
    pub remote: bool,
    status: CacheStatus,
    // Present unless a cache miss
    #[serde(skip_serializing_if = "Option::is_none")]
    source: Option<CacheSource>,
    // 0 if a cache miss
    time_saved: u64,
    /// The HEAD commit SHA that produced the cache entry, if available.
    #[serde(skip_serializing_if = "Option::is_none")]
    sha: Option<String>,
    /// A hash of uncommitted changes when the cache entry was produced.
    #[serde(skip_serializing_if = "Option::is_none")]
    dirty_hash: Option<String>,
}

#[derive(Debug, Serialize, Copy, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum CacheStatus {
    Hit,
    Miss,
}

#[derive(Debug, Serialize, Copy, Clone)]
#[serde(rename_all = "UPPERCASE")]
enum CacheSource {
    Local,
    Remote,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TaskSummary {
    pub task_id: TaskId<'static>,
    pub task: String,
    pub package: String,
    #[serde(flatten)]
    pub shared: SharedTaskSummary<TaskId<'static>>,
}

impl TaskSummaryInfo for TaskSummary {
    fn task_id(&self) -> &TaskId<'static> {
        &self.task_id
    }
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SinglePackageTaskSummary {
    pub task_id: String,
    pub task: String,
    #[serde(flatten)]
    pub shared: SharedTaskSummary<String>,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SharedTaskSummary<T> {
    pub hash: Arc<str>,
    pub inputs: BTreeMap<RelativeUnixPathBuf, String>,
    pub hash_of_external_dependencies: String,
    pub cache: TaskCacheSummary,
    pub command: String,
    pub cli_arguments: Vec<String>,
    pub outputs: Option<Vec<String>>,
    pub excluded_outputs: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub log_file: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub directory: Option<String>,
    pub dependencies: Vec<T>,
    pub dependents: Vec<T>,
    pub with: Vec<String>,
    pub resolved_task_definition: TaskSummaryTaskDefinition,
    pub expanded_outputs: Vec<AnchoredSystemPathBuf>,
    pub framework: String,
    pub env_mode: EnvMode,
    pub environment_variables: TaskEnvVarSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub execution: Option<TaskExecutionSummary>,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TaskEnvConfiguration {
    pub env: Vec<String>,
    pub pass_through_env: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct TaskSummaryTaskDefinition {
    outputs: Vec<String>,
    cache: bool,
    depends_on: Vec<String>,
    inputs: Vec<String>,
    output_logs: OutputLogsMode,
    persistent: bool,
    interruptible: bool,
    env: Vec<String>,
    pass_through_env: Option<Vec<String>>,
    interactive: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    env_mode: Option<EnvMode>,
    #[serde(skip_serializing_if = "Option::is_none")]
    incremental: Option<Vec<IncrementalPartitionSummary>>,
}

/// Summary of a single incremental partition for `--summarize` and `--dry`
/// output.
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct IncrementalPartitionSummary {
    pub outputs: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub inputs: Vec<String>,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TaskEnvVarSummary {
    pub specified: TaskEnvConfiguration,
    pub configured: Vec<String>,
    pub inferred: Vec<String>,
    #[serde(rename = "passthrough")]
    pub pass_through: Option<Vec<String>>,
}

impl TaskCacheSummary {
    // Used in observability/otel.rs to populate TaskMetricsPayload.cache_status
    pub(crate) fn status(&self) -> CacheStatus {
        self.status
    }

    // Used in observability/otel.rs to populate TaskMetricsPayload.cache_source
    pub(crate) fn cache_source_label(&self) -> Option<&'static str> {
        self.source.map(|source| match source {
            CacheSource::Local => "LOCAL",
            CacheSource::Remote => "REMOTE",
        })
    }

    pub fn cache_miss() -> Self {
        Self {
            local: false,
            remote: false,
            status: CacheStatus::Miss,
            time_saved: 0,
            source: None,
            sha: None,
            dirty_hash: None,
        }
    }
}

impl From<Option<CacheHitMetadata>> for TaskCacheSummary {
    fn from(response: Option<CacheHitMetadata>) -> Self {
        match response {
            Some(cache_hit) => {
                let source = CacheSource::from(cache_hit.source);
                // Assign these deprecated fields Local and Remote based on the information
                // available in the itemStatus. Note that these fields are
                // problematic, because an ItemStatus isn't always the composite
                // of both local and remote caches. That means that an ItemStatus might say it
                // was a local cache hit, and we return remote: false here. That's misleading
                // because it does not mean that there is no remote cache hit,
                // it _could_ mean that we never checked the remote cache. These
                // fields are being deprecated for this reason.
                let (local, remote) = match source {
                    CacheSource::Local => (true, false),
                    CacheSource::Remote => (false, true),
                };
                Self {
                    local,
                    remote,
                    status: CacheStatus::Hit,
                    source: Some(source),
                    time_saved: cache_hit.time_saved,
                    sha: cache_hit.sha,
                    dirty_hash: cache_hit.dirty_hash,
                }
            }
            None => Self::cache_miss(),
        }
    }
}

impl From<Option<HashTrackerCacheHitMetadata>> for TaskCacheSummary {
    fn from(response: Option<HashTrackerCacheHitMetadata>) -> Self {
        match response {
            Some(metadata) => {
                let source = if metadata.local {
                    CacheSource::Local
                } else {
                    CacheSource::Remote
                };
                Self {
                    local: metadata.local,
                    remote: metadata.remote,
                    status: CacheStatus::Hit,
                    source: Some(source),
                    time_saved: metadata.time_saved,
                    sha: metadata.sha,
                    dirty_hash: metadata.dirty_hash,
                }
            }
            None => Self::cache_miss(),
        }
    }
}

impl From<turborepo_cache::CacheSource> for CacheSource {
    fn from(value: turborepo_cache::CacheSource) -> Self {
        match value {
            turborepo_cache::CacheSource::Local => Self::Local,
            turborepo_cache::CacheSource::Remote => Self::Remote,
        }
    }
}

impl TaskEnvVarSummary {
    pub fn new(
        task_definition: &TaskDefinition,
        env_vars: DetailedMap,
        env_at_execution_start: &EnvironmentVariableMap,
    ) -> Result<Self, turborepo_env::Error> {
        // TODO: this operation differs from the actual env that gets passed in during
        // task execution it should be unified, but first we should copy Go's
        // behavior as we try to match the implementations
        let pass_through = task_definition
            .pass_through_env
            .as_deref()
            .map(|pass_through_env| -> Result<_, turborepo_env::Error> {
                Ok(env_at_execution_start
                    .from_wildcards(pass_through_env)?
                    .to_secret_hashable())
            })
            .transpose()?;

        Ok(Self {
            specified: TaskEnvConfiguration {
                env: task_definition.env.clone(),
                pass_through_env: task_definition.pass_through_env.clone(),
            },
            configured: env_vars.by_source.explicit.to_secret_hashable(),
            inferred: env_vars.by_source.matching.to_secret_hashable(),
            pass_through,
        })
    }

    /// Create a TaskEnvVarSummary from HashTrackerDetailedMap.
    ///
    /// This method is used when the env vars come from the HashTrackerInfo
    /// trait (defined in turborepo-types) rather than directly from
    /// turborepo-env.
    pub fn from_hash_tracker(
        task_definition: &TaskDefinition,
        env_vars: HashTrackerDetailedMap,
        env_at_execution_start: &EnvironmentVariableMap,
    ) -> Result<Self, turborepo_env::Error> {
        let pass_through = task_definition
            .pass_through_env
            .as_deref()
            .map(|pass_through_env| -> Result<_, turborepo_env::Error> {
                Ok(env_at_execution_start
                    .from_wildcards(pass_through_env)?
                    .to_secret_hashable())
            })
            .transpose()?;

        Ok(Self {
            specified: TaskEnvConfiguration {
                env: task_definition.env.clone(),
                pass_through_env: task_definition.pass_through_env.clone(),
            },
            configured: env_vars.explicit,
            inferred: env_vars.matching,
            pass_through,
        })
    }
}

impl From<TaskSummary> for SinglePackageTaskSummary {
    fn from(value: TaskSummary) -> Self {
        let TaskSummary {
            task_id, shared, ..
        } = value;
        Self {
            task_id: task_id.task().to_string(),
            task: task_id.task().to_string(),
            shared: shared.into(),
        }
    }
}

impl From<SharedTaskSummary<TaskId<'static>>> for SharedTaskSummary<String> {
    fn from(value: SharedTaskSummary<TaskId<'static>>) -> Self {
        let SharedTaskSummary {
            hash,
            inputs,
            hash_of_external_dependencies,
            cache,
            command,
            cli_arguments,
            outputs,
            excluded_outputs,
            log_file,
            expanded_outputs,
            dependencies,
            dependents,
            resolved_task_definition,
            framework,
            execution,
            env_mode,
            environment_variables,
            with,
            ..
        } = value;
        Self {
            hash,
            inputs,
            hash_of_external_dependencies,
            cache,
            command,
            cli_arguments,
            outputs,
            excluded_outputs,
            log_file,
            directory: None,
            expanded_outputs,
            dependencies: dependencies
                .into_iter()
                .map(|task_id| task_id.task().to_string())
                .collect(),
            dependents: dependents
                .into_iter()
                .map(|task_id| task_id.task().to_string())
                .collect(),
            with,
            resolved_task_definition,
            framework,
            execution,
            env_mode,
            environment_variables,
        }
    }
}

impl From<TaskDefinition> for TaskSummaryTaskDefinition {
    fn from(value: TaskDefinition) -> Self {
        let TaskDefinition {
            outputs:
                TaskOutputs {
                    inclusions,
                    exclusions,
                },
            cache,
            mut env,
            pass_through_env,
            topological_dependencies,
            task_dependencies,
            mut inputs,
            output_logs,
            persistent,
            interruptible,
            interactive,
            env_mode,
            with: _,
            incremental,
        } = value;

        let mut outputs = inclusions;
        for exclusion in exclusions {
            outputs.push(format!("!{exclusion}"));
        }

        let mut depends_on =
            Vec::with_capacity(task_dependencies.len() + topological_dependencies.len());
        for task_dependency in task_dependencies {
            depends_on.push(task_dependency.to_string());
        }
        for topological_dependency in topological_dependencies {
            depends_on.push(format!("^{}", topological_dependency.as_inner()));
        }

        // These _should_ already be sorted when the TaskDefinition struct was
        // unmarshaled, but we want to ensure they're sorted on the way out
        // also, just in case something in the middle mutates the items.
        depends_on.sort();
        outputs.sort();
        env.sort();
        inputs.globs.sort();

        let incremental_summary = incremental.map(|partitions| {
            partitions
                .into_iter()
                .map(|p| {
                    let mut partition_outputs = p.outputs.inclusions;
                    for exclusion in p.outputs.exclusions {
                        partition_outputs.push(format!("!{exclusion}"));
                    }
                    partition_outputs.sort();
                    IncrementalPartitionSummary {
                        outputs: partition_outputs,
                        inputs: p.inputs,
                    }
                })
                .collect()
        });

        Self {
            outputs,
            cache,
            depends_on,
            inputs: inputs.globs,
            output_logs,
            persistent,
            interruptible,
            interactive,
            env,
            pass_through_env,
            env_mode,
            incremental: incremental_summary,
        }
    }
}

#[cfg(test)]
mod test {
    use serde_json::json;
    use test_case::test_case;

    use super::*;

    #[test_case(CacheStatus::Hit, json!("HIT") ; "hit")]
    #[test_case(CacheStatus::Miss, json!("MISS") ; "miss")]
    #[test_case(CacheSource::Local, json!("LOCAL") ; "local")]
    #[test_case(CacheSource::Remote, json!("REMOTE") ; "remote")]
    #[test_case(
        TaskCacheSummary::cache_miss(),
        serde_json::json!({
                "local": false,
                "remote": false,
                "status": "MISS",
                "timeSaved": 0,
            })
        ; "cache miss"
    )]
    #[test_case(
        TaskCacheSummary {
            local: true,
            remote: false,
            status: CacheStatus::Hit,
            source: Some(CacheSource::Local),
            time_saved: 6,
            sha: None,
            dirty_hash: None,
        },
        serde_json::json!({
                "local": true,
                "remote": false,
                "status": "HIT",
                "source": "LOCAL",
                "timeSaved": 6,
            })
        ; "local cache hit"
    )]
    #[test_case(
        TaskCacheSummary {
            local: true,
            remote: false,
            status: CacheStatus::Hit,
            source: Some(CacheSource::Local),
            time_saved: 6,
            sha: Some("abc123".to_string()),
            dirty_hash: Some("def456".to_string()),
        },
        serde_json::json!({
                "local": true,
                "remote": false,
                "status": "HIT",
                "source": "LOCAL",
                "timeSaved": 6,
                "sha": "abc123",
                "dirtyHash": "def456",
            })
        ; "local cache hit with scm state"
    )]
    #[test_case(
        TaskSummaryTaskDefinition {
            outputs: vec!["foo".into()],
            cache: true,
            ..Default::default()
        },
        json!({
            "outputs": ["foo"],
            "cache": true,
            "dependsOn": [],
            "inputs": [],
            "outputLogs": "full",
            "persistent": false,
            "interruptible": false,
            "interactive": false,
            "env": [],
            "passThroughEnv": null,
        })
        ; "resolved task definition"
    )]
    fn test_serialization(
        value: impl serde::Serialize,
        expected: serde_json::Value,
    ) -> Result<(), serde_json::Error> {
        assert_eq!(serde_json::to_value(value)?, expected);
        Ok(())
    }
}
