use std::fmt;

use chrono::{DateTime, Local};
use serde::Serialize;
use tokio::sync::mpsc;
use turbopath::{AbsoluteSystemPathBuf, AnchoredSystemPath};
use turborepo_task_id::TaskId;
use turborepo_ui::{BOLD, BOLD_GREEN, BOLD_RED, ColorConfig, MAGENTA, color};

use crate::{TurboDuration, task::TaskExecutionSummary};

// Just used to make changing the type that gets passed to the state management
// thread easy
type Message = TrackerMessage;

// Should *not* be exposed outside of run summary module
/// Spawns task trackers and records the final state of all tasks
#[derive(Debug)]
pub struct ExecutionTracker {
    // this thread handles the state management
    state_thread: tokio::task::JoinHandle<SummaryState>,
    sender: mpsc::Sender<Message>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionSummary<'a> {
    // a synthesized turbo command to produce this invocation
    command: String,
    // the (possibly empty) path from the turborepo root to where the command was run
    #[serde(rename = "repoPath")]
    repo_path: &'a AnchoredSystemPath,
    // number of tasks that exited successfully (does not include cache hits)
    success: usize,
    // number of tasks that exited with failure
    failed: usize,
    // number of tasks that had a cache hit
    cached: usize,
    // number of tasks that started
    attempted: usize,
    pub start_time: i64,
    pub end_time: i64,
    #[serde(skip)]
    duration: TurboDuration,
    pub exit_code: i32,
}

impl<'a> ExecutionSummary<'a> {
    pub fn new(
        command: String,
        state: SummaryState,
        package_inference_root: Option<&'a AnchoredSystemPath>,
        exit_code: i32,
        start_time: DateTime<Local>,
        end_time: DateTime<Local>,
    ) -> Self {
        let duration = TurboDuration::new(&start_time, &end_time);
        Self {
            command,
            success: state.success,
            failed: state.failed,
            cached: state.cached,
            attempted: state.attempted,
            // We're either at some path in the repo, or at the root, which is an empty path
            repo_path: package_inference_root.unwrap_or_else(|| AnchoredSystemPath::empty()),
            start_time: start_time.timestamp_millis(),
            end_time: end_time.timestamp_millis(),
            duration,
            exit_code,
        }
    }

    /// We implement this on `ExecutionSummary` and not `RunSummary` because
    /// the `execution` field is nullable (due to normalize).
    pub fn print<T: TaskSummaryInfo>(
        &self,
        ui: ColorConfig,
        path: AbsoluteSystemPathBuf,
        failed_tasks: Vec<&T>,
    ) {
        let maybe_full_turbo = if self.cached == self.attempted && self.attempted > 0 {
            match std::env::var("TERM_PROGRAM").as_deref() {
                Ok("Apple_Terminal") => color!(ui, MAGENTA, ">>> FULL TURBO").to_string(),
                _ => ui.rainbow(">>> FULL TURBO").to_string(),
            }
        } else {
            String::new()
        };

        let mut line_data = vec![
            (
                "Tasks",
                format!(
                    "{}, {} total",
                    color!(ui, BOLD_GREEN, "{} successful", self.successful()),
                    self.attempted
                ),
            ),
            (
                "Cached",
                format!(
                    "{}, {} total",
                    color!(ui, BOLD, "{} cached", self.cached),
                    self.attempted
                )
                .to_string(),
            ),
            (
                "Time",
                format!(
                    "{} {}",
                    color!(ui, BOLD, "{}", self.duration),
                    maybe_full_turbo
                ),
            ),
        ];

        if path.exists() {
            line_data.push(("Summary", path.to_string()));
        }

        if !failed_tasks.is_empty() {
            let mut formatted: Vec<_> = failed_tasks
                .iter()
                .map(|task| color!(ui, BOLD_RED, "{}", task.task_id()).to_string())
                .collect();
            formatted.sort();
            line_data.push(("Failed", formatted.join(", ")));
        }

        let max_length = line_data
            .iter()
            .map(|(header, _)| header.len())
            .max()
            .unwrap_or_default();

        if self.attempted == 0 {
            turborepo_log::warn(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Summary),
                "No tasks were executed as part of this run.",
            )
            .emit();
        }

        // All output goes through the Logger. The TerminalSink renders
        // ANSI codes for color; the StructuredLogSink strips them.
        turborepo_log::info(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Summary),
            "",
        )
        .emit();
        for (header, trailer) in &line_data {
            turborepo_log::info(
                turborepo_log::Source::turbo(turborepo_log::Subsystem::Summary),
                format!(
                    "{}",
                    color!(
                        ui,
                        BOLD,
                        "{}{}:    {}",
                        " ".repeat(max_length - header.len()),
                        header,
                        trailer
                    )
                ),
            )
            .emit();
        }
        turborepo_log::info(
            turborepo_log::Source::turbo(turborepo_log::Subsystem::Summary),
            "",
        )
        .emit();
    }

    fn successful(&self) -> usize {
        self.success + self.cached
    }

    // Used in observability/otel.rs to populate RunMetricsPayload.attempted_tasks
    pub(crate) fn attempted(&self) -> usize {
        self.attempted
    }

    // Used in observability/otel.rs to populate RunMetricsPayload.failed_tasks
    pub(crate) fn failed(&self) -> usize {
        self.failed
    }

    // Used in observability/otel.rs to populate RunMetricsPayload.cached_tasks
    pub(crate) fn cached(&self) -> usize {
        self.cached
    }
}

/// Trait for types that can provide task summary information for printing
pub trait TaskSummaryInfo {
    fn task_id(&self) -> &TaskId<'static>;
}

/// The final states of all task executions
#[derive(Debug, Default, Clone)]
pub struct SummaryState {
    pub attempted: usize,
    pub failed: usize,
    pub cached: usize,
    pub success: usize,
    pub tasks: Vec<TaskState>,
}

#[derive(Debug, Clone)]
pub struct TaskState {
    pub task_id: TaskId<'static>,
    pub execution: Option<TaskExecutionSummary>,
}

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

impl SummaryState {
    fn handle_event(&mut self, event: Event) {
        match event {
            Event::Building => self.attempted += 1,
            Event::BuildFailed => self.failed += 1,
            Event::Cached => self.cached += 1,
            Event::Built => self.success += 1,
            Event::Canceled => (),
        }
    }
}

/// A tracker constructed for each task and used to communicate task events back
/// to the execution summary.
pub struct TaskTracker<T> {
    sender: mpsc::Sender<Message>,
    started_at: T,
    task_id: TaskId<'static>,
}

#[derive(Debug, Clone)]
struct TrackerMessage {
    event: Event,
    // Only present if task is finished
    state: Option<TaskState>,
}

#[derive(Debug, Clone, Copy, Serialize)]
enum Event {
    Building,
    BuildFailed,
    Cached,
    Built,
    // Canceled due to external signal or internal failure
    Canceled,
}

impl Default for ExecutionTracker {
    fn default() -> Self {
        Self::new()
    }
}

impl ExecutionTracker {
    pub fn new() -> Self {
        // This buffer size is probably overkill, but since messages are only a byte
        // it's worth the extra memory to avoid the channel filling up.
        let (sender, mut receiver) = mpsc::channel::<Message>(128);
        let state_thread = tokio::spawn(async move {
            let mut state = SummaryState::default();
            while let Some(TrackerMessage {
                event,
                state: task_state,
            }) = receiver.recv().await
            {
                state.handle_event(event);
                if let Some(task_state) = task_state {
                    state.tasks.push(task_state);
                }
            }
            state
        });

        Self {
            state_thread,
            sender,
        }
    }

    // Produce a tracker for the task
    pub fn task_tracker(&self, task_id: TaskId<'static>) -> TaskTracker<()> {
        TaskTracker {
            sender: self.sender.clone(),
            task_id,
            started_at: (),
        }
    }

    pub async fn finish(self) -> Result<SummaryState, tokio::task::JoinError> {
        let Self {
            state_thread,
            sender,
            ..
        } = self;
        // We drop the sender so the channel closes once all trackers have finished.
        // We don't explicitly close as that would cause running trackers to be unable
        // to send their execution summary.
        drop(sender);

        let summary_state = state_thread.await?;

        Ok(summary_state)
    }
}

impl TaskTracker<()> {
    /// Create a tracker whose events are silently discarded.
    /// Used for fire-and-forget persistent tasks in watch mode where the
    /// real tracker must be dropped to unblock `ExecutionTracker::finish()`.
    pub fn noop(task_id: TaskId<'static>) -> Self {
        // The receiver is dropped immediately; sends will silently fail.
        let (sender, _) = mpsc::channel(1);
        Self {
            sender,
            task_id,
            started_at: (),
        }
    }

    // Start the tracker
    pub async fn start(self) -> TaskTracker<DateTime<Local>> {
        let TaskTracker {
            sender, task_id, ..
        } = self;
        let started_at = Local::now();
        sender
            .send(TrackerMessage {
                event: Event::Building,
                state: None,
            })
            .await
            .ok();
        TaskTracker {
            sender,
            started_at,
            task_id,
        }
    }

    // Track that the task would be executed
    pub async fn dry_run(self) {
        let Self {
            sender, task_id, ..
        } = self;

        sender
            .send(TrackerMessage {
                event: Event::Canceled,
                state: Some(TaskState {
                    task_id,
                    execution: None,
                }),
            })
            .await
            .ok();
    }
}

impl TaskTracker<chrono::DateTime<Local>> {
    // In the case of a task getting canceled we send no information as there was an
    // internal turbo error
    pub fn cancel(self) {}

    pub async fn cached(self) -> TaskExecutionSummary {
        let Self {
            sender,
            started_at,
            task_id,
        } = self;

        let ended_at = Local::now();
        let execution = TaskExecutionSummary {
            start_time: started_at.timestamp_millis(),
            end_time: ended_at.timestamp_millis(),
            // Go synthesizes a zero exit code on cache hits
            exit_code: Some(0),
            error: None,
        };

        let state = TaskState {
            task_id,
            execution: Some(execution.clone()),
        };
        sender
            .send(TrackerMessage {
                event: Event::Cached,
                state: Some(state),
            })
            .await
            .ok();
        execution
    }

    pub async fn build_succeeded(self, exit_code: i32) -> TaskExecutionSummary {
        let Self {
            sender,
            started_at,
            task_id,
        } = self;

        let ended_at = Local::now();
        let execution = TaskExecutionSummary {
            start_time: started_at.timestamp_millis(),
            end_time: ended_at.timestamp_millis(),
            exit_code: Some(exit_code),
            error: None,
        };

        let state = TaskState {
            task_id,
            execution: Some(execution.clone()),
        };
        sender
            .send(TrackerMessage {
                event: Event::Built,
                state: Some(state),
            })
            .await
            .ok();
        execution
    }

    pub async fn build_failed(
        self,
        exit_code: Option<i32>,
        error: impl fmt::Display,
    ) -> TaskExecutionSummary {
        let Self {
            sender,
            started_at,
            task_id,
        } = self;

        let ended_at = Local::now();
        let execution = TaskExecutionSummary {
            start_time: started_at.timestamp_millis(),
            end_time: ended_at.timestamp_millis(),
            exit_code,
            error: Some(error.to_string()),
        };

        let state = TaskState {
            task_id,
            execution: Some(execution.clone()),
        };
        sender
            .send(TrackerMessage {
                event: Event::BuildFailed,
                state: Some(state),
            })
            .await
            .ok();
        execution
    }
}

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

    use super::*;

    #[tokio::test]
    async fn test_multiple_tasks() -> Result<(), Box<dyn std::error::Error>> {
        let summary = ExecutionTracker::new();
        let foo = TaskId::new("foo", "build");
        let bar = TaskId::new("bar", "build");
        let baz = TaskId::new("baz", "build");
        let boo = TaskId::new("boo", "build");
        let mut tasks = Vec::new();
        {
            let tracker = summary.task_tracker(foo.clone());
            tasks.push(tokio::spawn(async move {
                let tracker = tracker.start().await;
                tracker.build_succeeded(0).await;
            }));
        }
        {
            let tracker = summary.task_tracker(bar.clone());
            tasks.push(tokio::spawn(async move {
                let tracker = tracker.start().await;
                tracker.cached().await;
            }));
        }
        {
            let tracker = summary.task_tracker(baz.clone());
            tasks.push(tokio::spawn(async move {
                let tracker = tracker.start().await;
                tracker.build_failed(Some(1), "big bad error").await;
            }));
        }
        {
            let tracker = summary.task_tracker(boo.clone());
            tasks.push(tokio::spawn(async move {
                let tracker = tracker.start().await;
                tracker.cancel();
            }));
        }
        for task in tasks {
            task.await?;
        }

        let state = summary.finish().await?;
        assert_eq!(state.attempted, 4);
        assert_eq!(state.cached, 1);
        assert_eq!(state.failed, 1);
        assert_eq!(state.success, 1);
        let Some(foo_state) = state.tasks.iter().find(|task| task.task_id == foo) else {
            panic!("foo task should be present");
        };
        assert_eq!(
            foo_state
                .execution
                .as_ref()
                .map(|execution| execution.exit_code),
            Some(Some(0))
        );
        let Some(bar_state) = state.tasks.iter().find(|task| task.task_id == bar) else {
            panic!("bar task should be present");
        };
        assert_eq!(
            bar_state
                .execution
                .as_ref()
                .map(|execution| execution.exit_code),
            Some(Some(0))
        );
        let Some(baz_state) = state.tasks.iter().find(|task| task.task_id == baz) else {
            panic!("baz task should be present");
        };
        assert_eq!(
            baz_state
                .execution
                .as_ref()
                .map(|execution| execution.exit_code),
            Some(Some(1))
        );
        let boo_state = state.tasks.iter().find(|task| task.task_id == boo);
        assert!(
            boo_state.is_none(),
            "canceling doesn't produce execution data"
        );
        Ok(())
    }

    #[tokio::test]
    async fn test_timing() -> Result<(), Box<dyn std::error::Error>> {
        let summary = ExecutionTracker::new();
        let tracker = summary.task_tracker(TaskId::new("foo", "build"));
        let post_construction_time = Local::now().timestamp_millis();
        let sleep_duration = Duration::milliseconds(5);
        tokio::time::sleep(sleep_duration.to_std()?).await;

        let tracker = tracker.start().await;

        tokio::time::sleep(sleep_duration.to_std()?).await;
        tracker.build_succeeded(0).await;
        let mut state = summary.finish().await?;
        assert_eq!(state.tasks.len(), 1);
        let Some(task_state) = state.tasks.pop() else {
            panic!("task state should exist");
        };
        let Some(summary) = task_state.execution else {
            panic!("task execution should exist");
        };
        assert!(
            post_construction_time < summary.start_time,
            "tracker start time should start when start is called"
        );
        assert!(
            summary.start_time + sleep_duration.num_milliseconds() <= summary.end_time,
            "tracker end should be at least as long as the time between calls"
        );
        Ok(())
    }

    #[test_case(
        TaskExecutionSummary {
            start_time: 123,
            end_time: 234,
            exit_code: Some(0),
            error: None
        },
        json!({ "startTime": 123, "endTime": 234, "exitCode": 0 })
        ; "success"
    )]
    #[test_case(
        TaskExecutionSummary {
            start_time: 123,
            end_time: 234,
            exit_code: Some(1),
            error: Some("cannot find anything".into()),
        },
        json!({ "startTime": 123, "endTime": 234, "exitCode": 1, "error": "cannot find anything" })
        ; "failure"
    )]
    fn test_serialization(
        value: impl serde::Serialize,
        expected: serde_json::Value,
    ) -> Result<(), serde_json::Error> {
        assert_eq!(serde_json::to_value(value)?, expected);
        Ok(())
    }

    // Verifies that failed tasks can be identified directly from TaskState,
    // without needing the full TaskSummary machinery. This is the data path
    // the optimized (non-summary) finish will use.
    #[tokio::test]
    async fn test_failed_tasks_identifiable_from_task_state()
    -> Result<(), Box<dyn std::error::Error>> {
        let summary = ExecutionTracker::new();
        let success_task = TaskId::new("app", "build");
        let fail_task = TaskId::new("lib", "build");
        let cached_task = TaskId::new("utils", "build");

        let mut handles = Vec::new();
        {
            let tracker = summary.task_tracker(success_task.clone());
            handles.push(tokio::spawn(async move {
                tracker.start().await.build_succeeded(0).await;
            }));
        }
        {
            let tracker = summary.task_tracker(fail_task.clone());
            handles.push(tokio::spawn(async move {
                tracker.start().await.build_failed(Some(1), "uh oh").await;
            }));
        }
        {
            let tracker = summary.task_tracker(cached_task.clone());
            handles.push(tokio::spawn(async move {
                tracker.start().await.cached().await;
            }));
        }
        for h in handles {
            h.await?;
        }

        let state = summary.finish().await?;

        // TaskState.execution carries enough info to identify failures
        let failed: Vec<&TaskState> = state
            .tasks
            .iter()
            .filter(|t| t.execution.as_ref().is_some_and(|e| e.is_failure()))
            .collect();
        assert_eq!(failed.len(), 1);
        assert_eq!(failed[0].task_id, fail_task);

        // Counts are correct for ExecutionSummary construction
        assert_eq!(state.attempted, 3);
        assert_eq!(state.failed, 1);
        assert_eq!(state.success, 1);
        assert_eq!(state.cached, 1);
        Ok(())
    }

    // Verifies ExecutionSummary computes successful() correctly from SummaryState
    #[test]
    fn test_execution_summary_stats_from_state() {
        use turbopath::AnchoredSystemPath;

        let state = SummaryState {
            attempted: 10,
            failed: 2,
            cached: 5,
            success: 3,
            tasks: vec![],
        };

        let start = Local::now() - Duration::seconds(5);
        let end = Local::now();
        let summary = ExecutionSummary::new(
            "turbo run build".to_string(),
            state,
            Some(AnchoredSystemPath::empty()),
            1,
            start,
            end,
        );

        // successful = success + cached
        assert_eq!(summary.successful(), 8);
        assert_eq!(summary.attempted, 10);
        assert_eq!(summary.failed, 2);
        assert_eq!(summary.cached, 5);
        assert_eq!(summary.exit_code, 1);
    }

    // Regression tests for https://github.com/vercel/turborepo/issues/11527
    //
    // The crash occurs when a TaskTracker tries to send to the state thread
    // after its receiver has been dropped (e.g. during tokio runtime shutdown).
    // These tests verify that every send site handles a dead receiver
    // gracefully instead of panicking.
    //
    // We construct trackers with an already-closed channel to guarantee the
    // send path hits Err(SendError) — the exact condition from the bug.

    fn tracker_with_dead_channel() -> TaskTracker<()> {
        let (sender, receiver) = mpsc::channel::<Message>(1);
        drop(receiver);
        TaskTracker {
            sender,
            started_at: (),
            task_id: TaskId::new("pkg", "build"),
        }
    }

    #[tokio::test]
    async fn test_start_with_dead_receiver() {
        let tracker = tracker_with_dead_channel();
        // start() sends Event::Building — must not panic
        tracker.start().await;
    }

    #[tokio::test]
    async fn test_dry_run_with_dead_receiver() {
        let tracker = tracker_with_dead_channel();
        // dry_run() sends Event::Canceled — must not panic
        tracker.dry_run().await;
    }

    #[tokio::test]
    async fn test_cached_with_dead_receiver() {
        let (sender, receiver) = mpsc::channel::<Message>(1);
        drop(receiver);
        let tracker: TaskTracker<DateTime<Local>> = TaskTracker {
            sender,
            started_at: Local::now(),
            task_id: TaskId::new("pkg", "build"),
        };
        // cached() sends Event::Cached — must not panic
        tracker.cached().await;
    }

    #[tokio::test]
    async fn test_build_succeeded_with_dead_receiver() {
        let (sender, receiver) = mpsc::channel::<Message>(1);
        drop(receiver);
        let tracker: TaskTracker<DateTime<Local>> = TaskTracker {
            sender,
            started_at: Local::now(),
            task_id: TaskId::new("pkg", "build"),
        };
        // build_succeeded() sends Event::Built — must not panic
        tracker.build_succeeded(0).await;
    }

    #[tokio::test]
    async fn test_build_failed_with_dead_receiver() {
        let (sender, receiver) = mpsc::channel::<Message>(1);
        drop(receiver);
        let tracker: TaskTracker<DateTime<Local>> = TaskTracker {
            sender,
            started_at: Local::now(),
            task_id: TaskId::new("pkg", "build"),
        };
        // build_failed() sends Event::BuildFailed — must not panic
        tracker.build_failed(Some(1), "error").await;
    }
}
