use std::{
    future::poll_fn,
    path::PathBuf,
    time::Duration,
};

use async_io::Timer;
use freya::{
    prelude::{
        AccessibilityId,
        Focus,
        spawn_forever,
    },
    radio::{
        RadioChannel,
        RadioStation,
    },
};
use freya_terminal::prelude::*;
use portable_pty::CommandBuilder;

fn find_common_parent(paths: &[PathBuf]) -> PathBuf {
    if paths.is_empty() {
        return PathBuf::from(".");
    }
    if paths.len() == 1 {
        return paths[0].parent().unwrap_or(&paths[0]).to_path_buf();
    }

    let first_components: Vec<_> = paths[0].components().collect();
    for i in (0..first_components.len()).rev() {
        let candidate = first_components[..=i]
            .iter()
            .collect::<std::path::PathBuf>();
        if paths.iter().all(|p| p.starts_with(&candidate)) {
            return candidate;
        }
    }

    paths[0].parent().unwrap_or(&paths[0]).to_path_buf()
}

#[derive(Clone)]
pub struct TerminalState {
    pub id: usize,
    pub terminal_id: TerminalId,
    pub handle: Option<TerminalHandle>,
    pub focus_id: AccessibilityId,
    pub is_thinking: bool,
}

impl PartialEq for TerminalState {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
            && self.terminal_id == other.terminal_id
            && self.is_thinking == other.is_thinking
    }
}

#[derive(Clone, PartialEq)]
pub struct Instance {
    pub id: usize,
    pub project_id: usize,
    pub name: String,
    pub path: PathBuf,
    pub terminals: Vec<TerminalState>,
    pub focused_terminal_id: Option<AccessibilityId>,
}

#[derive(Clone, PartialEq)]
pub struct Project {
    pub id: usize,
    pub name: String,
    pub path: PathBuf,
}

#[derive(Clone, PartialEq)]
pub struct AppState {
    pub projects: Vec<Project>,
    pub instances: Vec<Instance>,
    pub font_size: f32,
    next_project_id: usize,
    next_instance_id: usize,
}

impl TerminalState {
    pub fn new(id: usize, cmd: CommandBuilder) -> Self {
        let terminal_id = TerminalId::new();
        let handle = TerminalHandle::new(terminal_id, cmd, None).ok();
        Self {
            id,
            handle,
            terminal_id,
            focus_id: Focus::new_id(),
            is_thinking: false,
        }
    }
}

impl Instance {
    pub fn new(id: usize, project_id: usize, name: String, path: PathBuf) -> Self {
        Self {
            id,
            project_id,
            name,
            path,
            terminals: Vec::new(),
            focused_terminal_id: None,
        }
    }

    pub fn create_default_terminals(&mut self) {
        for i in 0..4 {
            let shell = if i == 0 { "opencode" } else { "nu" };
            let mut cmd = CommandBuilder::new(shell);
            cmd.env("TERM", "xterm-256color");
            cmd.env("COLORTERM", "truecolor");
            cmd.env("LANG", "en_GB.UTF-8");
            cmd.cwd(&self.path);
            let terminal = TerminalState::new(i, cmd);
            self.terminals.push(terminal);
        }
        if let Some(first) = self.terminals.first() {
            self.focused_terminal_id = Some(first.focus_id);
        }
    }

    fn terminal_index_by_focus_id(&self, focus_id: AccessibilityId) -> Option<usize> {
        self.terminals.iter().position(|t| t.focus_id == focus_id)
    }
}

impl Project {
    pub fn new(id: usize, name: String, path: PathBuf) -> Self {
        Self { id, name, path }
    }

    pub fn list_subdirectories(&self) -> Vec<PathBuf> {
        let Ok(entries) = std::fs::read_dir(&self.path) else {
            return Vec::new();
        };
        let mut dirs: Vec<(PathBuf, std::time::SystemTime)> = entries
            .filter_map(|entry| {
                let entry = entry.ok()?;
                entry.file_type().ok()?.is_dir().then_some(())?;
                let path = entry.path();
                let fs_modified = entry.metadata().ok()?.modified().ok()?;
                let modified = self.last_activity_time(&path).unwrap_or(fs_modified);
                Some((path, modified))
            })
            .collect();
        dirs.sort_by(|a, b| b.1.cmp(&a.1));
        dirs.into_iter().map(|(path, _)| path).collect()
    }

    /// Returns the most recent time across: HEAD commit, git index and directory mtime.
    fn last_activity_time(&self, dir: &std::path::Path) -> Option<std::time::SystemTime> {
        let repo = git2::Repository::open(dir).ok()?;
        let commit_time = repo.head().ok()?.peel_to_commit().ok()?.time();
        let commit_time = std::time::UNIX_EPOCH
            .checked_add(std::time::Duration::from_secs(commit_time.seconds() as u64))?;

        // repo.path() resolves to the actual git dir, even for worktrees
        let index_time = std::fs::metadata(repo.path().join("index"))
            .and_then(|m| m.modified())
            .ok();

        let dir_time = std::fs::metadata(dir).and_then(|m| m.modified()).ok();

        [Some(commit_time), index_time, dir_time]
            .into_iter()
            .flatten()
            .max()
    }
}

impl AppState {
    pub fn new() -> Self {
        let mut state = Self {
            projects: Vec::new(),
            instances: Vec::new(),
            font_size: 13.0,
            next_project_id: 0,
            next_instance_id: 0,
        };

        if let Some(cli) = super::cli::CLI_ARGS.get() {
            for project_path in &cli.project {
                let path = PathBuf::from(project_path);
                if let Ok(canonical) = std::fs::canonicalize(&path) {
                    if canonical.is_dir() {
                        let name = canonical
                            .file_name()
                            .map(|n| n.to_string_lossy().to_string())
                            .unwrap_or_else(|| "Project".to_string());
                        state.add_project(name, canonical);
                    }
                }
            }

            if !cli.instance.is_empty() {
                let instance_paths: Vec<PathBuf> = cli
                    .instance
                    .iter()
                    .map(PathBuf::from)
                    .filter_map(|p| {
                        let canonical = std::fs::canonicalize(&p).ok()?;
                        if canonical.is_dir() {
                            Some(canonical)
                        } else {
                            None
                        }
                    })
                    .collect();

                if !instance_paths.is_empty() {
                    let common_parent = find_common_parent(&instance_paths);

                    let project_id = state
                        .projects
                        .iter()
                        .find(|p| {
                            common_parent.starts_with(&p.path) || p.path.starts_with(&common_parent)
                        })
                        .map(|p| p.id)
                        .unwrap_or_else(|| {
                            let name = common_parent
                                .file_name()
                                .map(|n| n.to_string_lossy().to_string())
                                .unwrap_or_else(|| "Project".to_string());
                            state.add_project(name, common_parent)
                        });

                    for path in instance_paths {
                        let name = path
                            .file_name()
                            .map(|n| n.to_string_lossy().to_string())
                            .unwrap_or_else(|| "Instance".to_string());
                        state.add_instance(project_id, name, path);
                    }
                }
            }
        }

        state
    }

    pub fn add_project(&mut self, name: String, path: PathBuf) -> usize {
        let id = self.next_project_id;
        self.next_project_id += 1;
        self.projects.push(Project::new(id, name, path));
        id
    }

    pub fn add_instance(&mut self, project_id: usize, name: String, path: PathBuf) -> usize {
        let id = self.next_instance_id;
        self.next_instance_id += 1;
        let mut instance = Instance::new(id, project_id, name, path);
        instance.create_default_terminals();
        self.instances.push(instance);
        id
    }

    pub fn remove_project(&mut self, project_id: usize) {
        self.instances.retain(|i| i.project_id != project_id);
        self.projects.retain(|p| p.id != project_id);
    }

    pub fn remove_instance(&mut self, instance_id: usize) {
        self.instances.retain(|i| i.id != instance_id);
    }

    pub fn focus_terminal_right(&mut self, instance_id: usize) {
        if let Some(instance) = self.instances.iter_mut().find(|i| i.id == instance_id) {
            let Some(focus_id) = instance.focused_terminal_id else {
                return;
            };
            let Some(idx) = instance.terminal_index_by_focus_id(focus_id) else {
                return;
            };
            let row = idx / 2;
            let col = idx % 2;

            if col < 1 {
                let new_idx = row * 2 + col + 1;
                if let Some(t) = instance.terminals.get(new_idx) {
                    instance.focused_terminal_id = Some(t.focus_id);
                    Focus::new_for_id(t.focus_id).request_focus();
                }
            }
        }
    }

    pub fn focus_terminal_left(&mut self, instance_id: usize) {
        if let Some(instance) = self.instances.iter_mut().find(|i| i.id == instance_id) {
            let Some(focus_id) = instance.focused_terminal_id else {
                return;
            };
            let Some(idx) = instance.terminal_index_by_focus_id(focus_id) else {
                return;
            };
            let row = idx / 2;
            let col = idx % 2;

            if col > 0 {
                let new_idx = row * 2 + col - 1;
                if let Some(t) = instance.terminals.get(new_idx) {
                    instance.focused_terminal_id = Some(t.focus_id);
                    Focus::new_for_id(t.focus_id).request_focus();
                }
            }
        }
    }

    pub fn focus_terminal_down(&mut self, instance_id: usize) {
        if let Some(instance) = self.instances.iter_mut().find(|i| i.id == instance_id) {
            let Some(focus_id) = instance.focused_terminal_id else {
                return;
            };
            let Some(idx) = instance.terminal_index_by_focus_id(focus_id) else {
                return;
            };
            let row = idx / 2;
            let col = idx % 2;

            if row < 1 {
                let new_idx = (row + 1) * 2 + col;
                if let Some(t) = instance.terminals.get(new_idx) {
                    instance.focused_terminal_id = Some(t.focus_id);
                    Focus::new_for_id(t.focus_id).request_focus();
                }
            }
        }
    }

    pub fn focus_terminal_up(&mut self, instance_id: usize) {
        if let Some(instance) = self.instances.iter_mut().find(|i| i.id == instance_id) {
            let Some(focus_id) = instance.focused_terminal_id else {
                return;
            };
            let Some(idx) = instance.terminal_index_by_focus_id(focus_id) else {
                return;
            };
            let row = idx / 2;
            let col = idx % 2;

            if row > 0 {
                let new_idx = (row - 1) * 2 + col;
                if let Some(t) = instance.terminals.get(new_idx) {
                    instance.focused_terminal_id = Some(t.focus_id);
                    Focus::new_for_id(t.focus_id).request_focus();
                }
            }
        }
    }
}

#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum AppChannel {
    App,
    Instance(usize),
    Instances,
}

impl RadioChannel<AppState> for AppChannel {
    fn derive_channel(self, state: &AppState) -> Vec<Self> {
        match self {
            AppChannel::App => {
                // App changes may affect all instances (e.g., remove_project)
                let mut channels = vec![AppChannel::App];
                for instance in &state.instances {
                    channels.push(AppChannel::Instance(instance.id));
                }
                channels
            }
            AppChannel::Instances => {
                let mut channels = vec![AppChannel::Instances];
                for instance in &state.instances {
                    channels.push(AppChannel::Instance(instance.id));
                }
                channels
            }
            AppChannel::Instance(id) => vec![AppChannel::Instance(id)],
        }
    }
}

/// Spawn a background task that monitors PTY output to toggle `is_thinking`.
/// Self-terminates when the PTY closes.
pub fn spawn_thinking_watcher(
    mut station: RadioStation<AppState, AppChannel>,
    instance_id: usize,
    terminal_index: usize,
    handle: TerminalHandle,
) {
    spawn_forever(async move {
        loop {
            let closed = handle.closed();
            let output = handle.output_received();
            futures_util::pin_mut!(closed);
            futures_util::pin_mut!(output);

            match futures_util::future::select(output, closed).await {
                futures_util::future::Either::Right(_) => break,
                futures_util::future::Either::Left(_) => {}
            }

            if handle.last_write_elapsed() < Duration::from_secs(1) {
                continue;
            }

            if let Some(term) = station
                .write_channel(AppChannel::Instance(instance_id))
                .instances
                .iter_mut()
                .find(|i| i.id == instance_id)
                .and_then(|inst| inst.terminals.get_mut(terminal_index))
            {
                term.is_thinking = true;
            }

            loop {
                Timer::after(Duration::from_secs(1)).await;
                let mut fut = std::pin::pin!(handle.output_received());
                let has_output =
                    poll_fn(|cx| std::task::Poll::Ready(fut.as_mut().poll(cx).is_ready())).await;
                if !has_output {
                    break;
                }
            }

            if let Some(term) = station
                .write_channel(AppChannel::Instance(instance_id))
                .instances
                .iter_mut()
                .find(|i| i.id == instance_id)
                .and_then(|inst| inst.terminals.get_mut(terminal_index))
            {
                term.is_thinking = false;
            }
        }
    });
}
