use std::time::Duration;
use std::time::Instant;

// Environment filter data models for the TUI
#[derive(Clone, Debug, Default)]
pub struct EnvironmentRow {
    pub id: String,
    pub label: Option<String>,
    pub is_pinned: bool,
    pub repo_hints: Option<String>, // e.g., "openai/codex"
}

#[derive(Clone, Debug, Default)]
pub struct EnvModalState {
    pub query: String,
    pub selected: usize,
}

#[derive(Clone, Debug, Default)]
pub struct BestOfModalState {
    pub selected: usize,
}

#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum ApplyResultLevel {
    Success,
    Partial,
    Error,
}

#[derive(Clone, Debug)]
pub struct ApplyModalState {
    pub task_id: TaskId,
    pub title: String,
    pub result_message: Option<String>,
    pub result_level: Option<ApplyResultLevel>,
    pub skipped_paths: Vec<String>,
    pub conflict_paths: Vec<String>,
    pub diff_override: Option<String>,
}

use crate::scrollable_diff::ScrollableDiff;
use codex_cloud_tasks_client::CloudBackend;
use codex_cloud_tasks_client::TaskId;
use codex_cloud_tasks_client::TaskSummary;
#[derive(Default)]
pub struct App {
    pub tasks: Vec<TaskSummary>,
    pub selected: usize,
    pub status: String,
    pub diff_overlay: Option<DiffOverlay>,
    pub spinner_start: Option<Instant>,
    pub refresh_inflight: bool,
    pub details_inflight: bool,
    // Environment filter state
    pub env_filter: Option<String>,
    pub env_modal: Option<EnvModalState>,
    pub apply_modal: Option<ApplyModalState>,
    pub best_of_modal: Option<BestOfModalState>,
    pub environments: Vec<EnvironmentRow>,
    pub env_last_loaded: Option<std::time::Instant>,
    pub env_loading: bool,
    pub env_error: Option<String>,
    // New Task page
    pub new_task: Option<crate::new_task::NewTaskPage>,
    pub best_of_n: usize,
    // Apply preflight spinner state
    pub apply_preflight_inflight: bool,
    // Apply action spinner state
    pub apply_inflight: bool,
    // Background enrichment coordination
    pub list_generation: u64,
    pub in_flight: std::collections::HashSet<String>,
    // Background enrichment caches were planned; currently unused.
}

impl App {
    pub fn new() -> Self {
        Self {
            tasks: Vec::new(),
            selected: 0,
            status: "Press r to refresh".to_string(),
            diff_overlay: None,
            spinner_start: None,
            refresh_inflight: false,
            details_inflight: false,
            env_filter: None,
            env_modal: None,
            apply_modal: None,
            best_of_modal: None,
            environments: Vec::new(),
            env_last_loaded: None,
            env_loading: false,
            env_error: None,
            new_task: None,
            best_of_n: 1,
            apply_preflight_inflight: false,
            apply_inflight: false,
            list_generation: 0,
            in_flight: std::collections::HashSet::new(),
        }
    }

    pub fn next(&mut self) {
        if self.tasks.is_empty() {
            return;
        }
        self.selected = (self.selected + 1).min(self.tasks.len().saturating_sub(1));
    }

    pub fn prev(&mut self) {
        if self.tasks.is_empty() {
            return;
        }
        if self.selected > 0 {
            self.selected -= 1;
        }
    }
}

pub async fn load_tasks(
    backend: &dyn CloudBackend,
    env: Option<&str>,
) -> anyhow::Result<Vec<TaskSummary>> {
    // In later milestones, add a small debounce, spinner, and error display.
    let tasks = tokio::time::timeout(
        Duration::from_secs(5),
        backend.list_tasks(env, Some(20), /*cursor*/ None),
    )
    .await??;
    // Hide review-only tasks from the main list.
    let filtered: Vec<TaskSummary> = tasks.tasks.into_iter().filter(|t| !t.is_review).collect();
    Ok(filtered)
}

pub struct DiffOverlay {
    pub title: String,
    pub task_id: TaskId,
    pub sd: ScrollableDiff,
    pub base_can_apply: bool,
    pub diff_lines: Vec<String>,
    pub text_lines: Vec<String>,
    pub prompt: Option<String>,
    pub attempts: Vec<AttemptView>,
    pub selected_attempt: usize,
    pub current_view: DetailView,
    pub base_turn_id: Option<String>,
    pub sibling_turn_ids: Vec<String>,
    pub attempt_total_hint: Option<usize>,
}

#[derive(Clone, Debug, Default)]
pub struct AttemptView {
    pub turn_id: Option<String>,
    pub status: codex_cloud_tasks_client::AttemptStatus,
    pub attempt_placement: Option<i64>,
    pub diff_lines: Vec<String>,
    pub text_lines: Vec<String>,
    pub prompt: Option<String>,
    pub diff_raw: Option<String>,
}

impl AttemptView {
    pub fn has_diff(&self) -> bool {
        !self.diff_lines.is_empty()
    }

    pub fn has_text(&self) -> bool {
        !self.text_lines.is_empty() || self.prompt.is_some()
    }
}

impl DiffOverlay {
    pub fn new(task_id: TaskId, title: String, attempt_total_hint: Option<usize>) -> Self {
        let mut sd = ScrollableDiff::new();
        sd.set_content(Vec::new());
        Self {
            title,
            task_id,
            sd,
            base_can_apply: false,
            diff_lines: Vec::new(),
            text_lines: Vec::new(),
            prompt: None,
            attempts: vec![AttemptView::default()],
            selected_attempt: 0,
            current_view: DetailView::Prompt,
            base_turn_id: None,
            sibling_turn_ids: Vec::new(),
            attempt_total_hint,
        }
    }

    pub fn current_attempt(&self) -> Option<&AttemptView> {
        self.attempts.get(self.selected_attempt)
    }

    pub fn base_attempt_mut(&mut self) -> &mut AttemptView {
        if self.attempts.is_empty() {
            self.attempts.push(AttemptView::default());
        }
        &mut self.attempts[0]
    }

    pub fn set_view(&mut self, view: DetailView) {
        self.current_view = view;
        self.apply_selection_to_fields();
    }

    pub fn expected_attempts(&self) -> Option<usize> {
        self.attempt_total_hint.or({
            if self.attempts.is_empty() {
                None
            } else {
                Some(self.attempts.len())
            }
        })
    }

    pub fn attempt_count(&self) -> usize {
        self.attempts.len()
    }

    pub fn attempt_display_total(&self) -> usize {
        self.expected_attempts()
            .unwrap_or_else(|| self.attempts.len().max(1))
    }

    pub fn step_attempt(&mut self, delta: isize) -> bool {
        let total = self.attempts.len();
        if total <= 1 {
            return false;
        }
        let total_isize = total as isize;
        let current = self.selected_attempt as isize;
        let mut next = current + delta;
        next = ((next % total_isize) + total_isize) % total_isize;
        let next = next as usize;
        self.selected_attempt = next;
        self.apply_selection_to_fields();
        true
    }

    pub fn current_can_apply(&self) -> bool {
        matches!(self.current_view, DetailView::Diff)
            && self
                .current_attempt()
                .and_then(|attempt| attempt.diff_raw.as_ref())
                .map(|diff| !diff.is_empty())
                .unwrap_or(false)
    }

    pub fn apply_selection_to_fields(&mut self) {
        let (diff_lines, text_lines, prompt) = if let Some(attempt) = self.current_attempt() {
            (
                attempt.diff_lines.clone(),
                attempt.text_lines.clone(),
                attempt.prompt.clone(),
            )
        } else {
            self.diff_lines.clear();
            self.text_lines.clear();
            self.prompt = None;
            self.sd.set_content(vec!["<loading attempt>".to_string()]);
            return;
        };

        self.diff_lines = diff_lines.clone();
        self.text_lines = text_lines.clone();
        self.prompt = prompt;

        match self.current_view {
            DetailView::Diff => {
                if diff_lines.is_empty() {
                    self.sd.set_content(vec!["<no diff available>".to_string()]);
                } else {
                    self.sd.set_content(diff_lines);
                }
            }
            DetailView::Prompt => {
                if text_lines.is_empty() {
                    self.sd.set_content(vec!["<no output>".to_string()]);
                } else {
                    self.sd.set_content(text_lines);
                }
            }
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DetailView {
    Diff,
    Prompt,
}

/// Internal app events delivered from background tasks.
/// These let the UI event loop remain responsive and keep the spinner animating.
#[derive(Debug)]
pub enum AppEvent {
    TasksLoaded {
        env: Option<String>,
        result: anyhow::Result<Vec<TaskSummary>>,
    },
    // Background diff summary events were planned; removed for now to keep code minimal.
    /// Autodetection of a likely environment id finished
    EnvironmentAutodetected(anyhow::Result<crate::env_detect::AutodetectSelection>),
    /// Background completion of environment list fetch
    EnvironmentsLoaded(anyhow::Result<Vec<EnvironmentRow>>),
    DetailsDiffLoaded {
        id: TaskId,
        title: String,
        diff: String,
    },
    DetailsMessagesLoaded {
        id: TaskId,
        title: String,
        messages: Vec<String>,
        prompt: Option<String>,
        turn_id: Option<String>,
        sibling_turn_ids: Vec<String>,
        attempt_placement: Option<i64>,
        attempt_status: codex_cloud_tasks_client::AttemptStatus,
    },
    DetailsFailed {
        id: TaskId,
        title: String,
        error: String,
    },
    AttemptsLoaded {
        id: TaskId,
        attempts: Vec<codex_cloud_tasks_client::TurnAttempt>,
    },
    /// Background completion of new task submission
    NewTaskSubmitted(Result<codex_cloud_tasks_client::CreatedTask, String>),
    /// Background completion of apply preflight when opening modal or on demand
    ApplyPreflightFinished {
        id: TaskId,
        title: String,
        message: String,
        level: ApplyResultLevel,
        skipped: Vec<String>,
        conflicts: Vec<String>,
    },
    /// Background completion of apply action (actual patch application)
    ApplyFinished {
        id: TaskId,
        result: std::result::Result<codex_cloud_tasks_client::ApplyOutcome, String>,
    },
}

// Convenience aliases; currently unused.
#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use codex_cloud_tasks_client::CloudBackendFuture;
    use codex_cloud_tasks_client::CloudTaskError;

    struct FakeBackend {
        // maps env key to titles
        by_env: std::collections::HashMap<Option<String>, Vec<&'static str>>,
    }

    impl FakeBackend {
        async fn list_tasks(
            &self,
            env: Option<&str>,
            limit: Option<i64>,
            cursor: Option<&str>,
        ) -> Result<codex_cloud_tasks_client::TaskListPage, CloudTaskError> {
            let key = env.map(str::to_string);
            let titles = self
                .by_env
                .get(&key)
                .cloned()
                .unwrap_or_else(|| vec!["default-a", "default-b"]);
            let mut out = Vec::new();
            for (i, t) in titles.into_iter().enumerate() {
                out.push(TaskSummary {
                    id: TaskId(format!("T-{i}")),
                    title: t.to_string(),
                    status: codex_cloud_tasks_client::TaskStatus::Ready,
                    updated_at: Utc::now(),
                    environment_id: env.map(str::to_string),
                    environment_label: None,
                    summary: codex_cloud_tasks_client::DiffSummary::default(),
                    is_review: false,
                    attempt_total: Some(1),
                });
            }
            let max = limit.unwrap_or(i64::MAX);
            let max = max.min(20);
            let mut limited = Vec::new();
            for task in out {
                if (limited.len() as i64) >= max {
                    break;
                }
                limited.push(task);
            }
            Ok(codex_cloud_tasks_client::TaskListPage {
                tasks: limited,
                cursor: cursor.map(str::to_string),
            })
        }

        async fn get_task_summary(&self, id: TaskId) -> Result<TaskSummary, CloudTaskError> {
            self.list_tasks(/*env*/ None, /*limit*/ None, /*cursor*/ None)
                .await?
                .tasks
                .into_iter()
                .find(|t| t.id == id)
                .ok_or_else(|| CloudTaskError::Msg(format!("Task {} not found", id.0)))
        }

        async fn get_task_text(
            &self,
            _id: TaskId,
        ) -> Result<codex_cloud_tasks_client::TaskText, CloudTaskError> {
            Ok(codex_cloud_tasks_client::TaskText {
                prompt: Some("Example prompt".to_string()),
                messages: Vec::new(),
                turn_id: Some("fake-turn".to_string()),
                sibling_turn_ids: Vec::new(),
                attempt_placement: Some(0),
                attempt_status: codex_cloud_tasks_client::AttemptStatus::Completed,
            })
        }
    }

    impl codex_cloud_tasks_client::CloudBackend for FakeBackend {
        fn list_tasks<'a>(
            &'a self,
            env: Option<&'a str>,
            limit: Option<i64>,
            cursor: Option<&'a str>,
        ) -> CloudBackendFuture<'a, codex_cloud_tasks_client::TaskListPage> {
            Box::pin(FakeBackend::list_tasks(self, env, limit, cursor))
        }

        fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary> {
            Box::pin(FakeBackend::get_task_summary(self, id))
        }

        fn get_task_diff(&self, _id: TaskId) -> CloudBackendFuture<'_, Option<String>> {
            Box::pin(async {
                Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
                    "not used in test",
                ))
            })
        }

        fn get_task_messages(&self, _id: TaskId) -> CloudBackendFuture<'_, Vec<String>> {
            Box::pin(async { Ok(vec![]) })
        }

        fn get_task_text(
            &self,
            id: TaskId,
        ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::TaskText> {
            Box::pin(FakeBackend::get_task_text(self, id))
        }

        fn list_sibling_attempts(
            &self,
            _task: TaskId,
            _turn_id: String,
        ) -> CloudBackendFuture<'_, Vec<codex_cloud_tasks_client::TurnAttempt>> {
            Box::pin(async { Ok(Vec::new()) })
        }

        fn apply_task(
            &self,
            _id: TaskId,
            _diff_override: Option<String>,
        ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome> {
            Box::pin(async {
                Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
                    "not used in test",
                ))
            })
        }

        fn apply_task_preflight(
            &self,
            _id: TaskId,
            _diff_override: Option<String>,
        ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome> {
            Box::pin(async {
                Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
                    "not used in test",
                ))
            })
        }

        fn create_task<'a>(
            &'a self,
            _env_id: &'a str,
            _prompt: &'a str,
            _git_ref: &'a str,
            _qa_mode: bool,
            _best_of_n: usize,
        ) -> CloudBackendFuture<'a, codex_cloud_tasks_client::CreatedTask> {
            Box::pin(async {
                Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
                    "not used in test",
                ))
            })
        }
    }

    #[tokio::test]
    async fn load_tasks_uses_env_parameter() {
        // Arrange: env-specific task titles
        let mut by_env = std::collections::HashMap::new();
        by_env.insert(None, vec!["root-1", "root-2"]);
        by_env.insert(Some("env-A".to_string()), vec!["A-1"]);
        by_env.insert(Some("env-B".to_string()), vec!["B-1", "B-2", "B-3"]);
        let backend = FakeBackend { by_env };

        // Act + Assert
        let root = load_tasks(&backend, /*env*/ None).await.unwrap();
        assert_eq!(root.len(), 2);
        assert_eq!(root[0].title, "root-1");

        let a = load_tasks(&backend, Some("env-A")).await.unwrap();
        assert_eq!(a.len(), 1);
        assert_eq!(a[0].title, "A-1");

        let b = load_tasks(&backend, Some("env-B")).await.unwrap();
        assert_eq!(b.len(), 3);
        assert_eq!(b[2].title, "B-3");
    }
}
