use std::collections::{HashMap, HashSet};

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde_json::Value;

use crate::{
    controllers::regions::{
        MAX_TOTAL_REPLICAS, region_display_name, region_flag_name, region_full_label,
        region_is_available,
    },
    gql::queries,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScaleTuiAction {
    Continue,
    Apply(HashMap<String, u64>),
    Cancel,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScaleTuiMode {
    Browse,
    Edit,
    Confirm,
    Help,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScaleTuiFocus {
    Regions,
    Apply,
    Cancel,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegionRow {
    pub name: String,
    pub cli_name: String,
    pub label: String,
    pub current: u64,
    pub desired: u64,
    pub available: bool,
    pub dedicated: bool,
}

impl RegionRow {
    pub fn change(&self) -> i128 {
        self.desired as i128 - self.current as i128
    }

    pub fn changed(&self) -> bool {
        self.current != self.desired
    }
}

#[derive(Debug)]
pub struct ScaleTuiApp {
    pub service_name: String,
    pub environment_name: String,
    pub rows: Vec<RegionRow>,
    pub selected: usize,
    pub mode: ScaleTuiMode,
    pub focus: ScaleTuiFocus,
    pub edit_input: String,
    pub error: Option<String>,
}

impl ScaleTuiApp {
    pub fn new(
        service_name: String,
        environment_name: String,
        regions: queries::regions::ResponseData,
        existing: &Value,
    ) -> Self {
        let current = current_replicas(existing);
        let mut seen = HashSet::new();
        let mut rows = regions
            .regions
            .iter()
            .filter_map(|region| {
                let replicas = *current.get(&region.name).unwrap_or(&0);
                let available = region_is_available(region);
                if !available && replicas == 0 {
                    return None;
                }

                seen.insert(region.name.clone());
                Some(RegionRow {
                    name: region.name.clone(),
                    cli_name: region_flag_name(region),
                    label: region_full_label(region),
                    current: replicas,
                    desired: replicas,
                    available,
                    dedicated: region.workspace_id.is_some(),
                })
            })
            .collect::<Vec<_>>();

        for (name, replicas) in current {
            if seen.contains(&name) || replicas == 0 {
                continue;
            }

            rows.push(RegionRow {
                cli_name: name.clone(),
                label: region_display_name(&name, &HashMap::new()),
                name,
                current: replicas,
                desired: replicas,
                available: false,
                dedicated: false,
            });
        }

        rows.sort_by(|a, b| {
            b.current
                .cmp(&a.current)
                .then_with(|| a.label.cmp(&b.label))
                .then_with(|| a.name.cmp(&b.name))
        });

        Self {
            service_name,
            environment_name,
            rows,
            selected: 0,
            mode: ScaleTuiMode::Browse,
            focus: ScaleTuiFocus::Regions,
            edit_input: String::new(),
            error: None,
        }
    }

    pub fn visible_indices(&self) -> Vec<usize> {
        (0..self.rows.len()).collect()
    }

    pub fn selected_row(&self) -> Option<&RegionRow> {
        let visible = self.visible_indices();
        visible
            .get(self.selected)
            .and_then(|idx| self.rows.get(*idx))
    }

    pub fn changes(&self) -> HashMap<String, u64> {
        self.rows
            .iter()
            .filter(|row| row.changed())
            .map(|row| (row.name.clone(), row.desired))
            .collect()
    }

    pub fn changed_rows(&self) -> Vec<&RegionRow> {
        self.rows.iter().filter(|row| row.changed()).collect()
    }

    pub fn total_desired_replicas(&self) -> u64 {
        self.rows
            .iter()
            .fold(0u64, |total, row| total.saturating_add(row.desired))
    }

    pub fn command_preview(&self) -> String {
        let mut changes = self.changed_rows();
        changes.sort_by(|a, b| a.cli_name.cmp(&b.cli_name));

        if changes.is_empty() {
            return "No changes yet".to_string();
        }

        let mut parts = vec![
            "railway".to_string(),
            "scale".to_string(),
            "--environment".to_string(),
            shell_arg(&self.environment_name),
            "--service".to_string(),
            shell_arg(&self.service_name),
        ];
        parts.extend(
            changes
                .iter()
                .map(|row| format!("{}={}", self.command_region_name(row), row.desired)),
        );
        parts.join(" ")
    }

    fn command_region_name<'a>(&'a self, row: &'a RegionRow) -> &'a str {
        let duplicates = self
            .rows
            .iter()
            .filter(|candidate| candidate.cli_name == row.cli_name)
            .count();
        if row.cli_name.is_empty() || duplicates > 1 {
            &row.name
        } else {
            &row.cli_name
        }
    }

    pub fn handle_key(&mut self, key: KeyEvent) -> ScaleTuiAction {
        self.error = None;

        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
            return ScaleTuiAction::Cancel;
        }

        match self.mode {
            ScaleTuiMode::Browse => self.handle_browse_key(key),
            ScaleTuiMode::Edit => self.handle_edit_key(key),
            ScaleTuiMode::Confirm => self.handle_confirm_key(key),
            ScaleTuiMode::Help => self.handle_help_key(key),
        }
    }

    fn handle_browse_key(&mut self, key: KeyEvent) -> ScaleTuiAction {
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') => ScaleTuiAction::Cancel,
            KeyCode::Tab => {
                self.next_focus();
                ScaleTuiAction::Continue
            }
            KeyCode::BackTab => {
                self.previous_focus();
                ScaleTuiAction::Continue
            }
            KeyCode::Left | KeyCode::Char('h') if self.focus != ScaleTuiFocus::Regions => {
                self.previous_focus();
                ScaleTuiAction::Continue
            }
            KeyCode::Right | KeyCode::Char('l') if self.focus != ScaleTuiFocus::Regions => {
                self.next_focus();
                ScaleTuiAction::Continue
            }
            KeyCode::Up | KeyCode::Char('k') => {
                if self.focus == ScaleTuiFocus::Regions {
                    self.move_selection(-1);
                } else {
                    self.focus = ScaleTuiFocus::Regions;
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Down | KeyCode::Char('j') => {
                if self.focus == ScaleTuiFocus::Regions {
                    if self.selected + 1 >= self.visible_indices().len() {
                        self.focus = ScaleTuiFocus::Apply;
                    } else {
                        self.move_selection(1);
                    }
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Home | KeyCode::Char('g') => {
                self.focus = ScaleTuiFocus::Regions;
                self.selected = 0;
                ScaleTuiAction::Continue
            }
            KeyCode::End | KeyCode::Char('G') => {
                self.focus = ScaleTuiFocus::Regions;
                let len = self.visible_indices().len();
                self.selected = len.saturating_sub(1);
                ScaleTuiAction::Continue
            }
            KeyCode::Char('+') | KeyCode::Char('=') => {
                if self.focus == ScaleTuiFocus::Regions {
                    self.adjust_selected(1);
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Char('-') => {
                if self.focus == ScaleTuiFocus::Regions {
                    self.adjust_selected(-1);
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Char('0') => {
                if self.focus == ScaleTuiFocus::Regions {
                    self.set_selected_desired(0);
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Char(ch) if ch.is_ascii_digit() && self.focus == ScaleTuiFocus::Regions => {
                self.edit_input = ch.to_string();
                self.mode = ScaleTuiMode::Edit;
                ScaleTuiAction::Continue
            }
            KeyCode::Enter => match self.focus {
                ScaleTuiFocus::Regions => {
                    if let Some(row) = self.selected_row() {
                        self.edit_input = row.desired.to_string();
                        self.mode = ScaleTuiMode::Edit;
                    }
                    ScaleTuiAction::Continue
                }
                ScaleTuiFocus::Apply => self.activate_apply(),
                ScaleTuiFocus::Cancel => ScaleTuiAction::Cancel,
            },
            KeyCode::Char('e') => {
                if self.focus == ScaleTuiFocus::Regions {
                    if let Some(row) = self.selected_row() {
                        self.edit_input = row.desired.to_string();
                        self.mode = ScaleTuiMode::Edit;
                    }
                }
                ScaleTuiAction::Continue
            }
            KeyCode::Char('a') => self.activate_apply(),
            KeyCode::Char('?') => {
                self.mode = ScaleTuiMode::Help;
                ScaleTuiAction::Continue
            }
            _ => ScaleTuiAction::Continue,
        }
    }

    fn handle_edit_key(&mut self, key: KeyEvent) -> ScaleTuiAction {
        match key.code {
            KeyCode::Esc => {
                self.mode = ScaleTuiMode::Browse;
                self.focus = ScaleTuiFocus::Regions;
                self.edit_input.clear();
            }
            KeyCode::Enter => match self.edit_input.parse::<u64>() {
                Ok(replicas) => {
                    self.set_selected_desired(replicas);
                    self.mode = ScaleTuiMode::Browse;
                    self.focus = ScaleTuiFocus::Regions;
                    self.edit_input.clear();
                }
                Err(_) => {
                    self.error = Some("Replica count must be a whole number.".to_string());
                }
            },
            KeyCode::Backspace => {
                self.edit_input.pop();
            }
            KeyCode::Delete => {
                self.edit_input.clear();
            }
            KeyCode::Char(ch) if ch.is_ascii_digit() => {
                self.edit_input.push(ch);
            }
            _ => {}
        }
        ScaleTuiAction::Continue
    }

    fn handle_confirm_key(&mut self, key: KeyEvent) -> ScaleTuiAction {
        match key.code {
            KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('a') => {
                ScaleTuiAction::Apply(self.changes())
            }
            KeyCode::Esc | KeyCode::Char('e') => {
                self.mode = ScaleTuiMode::Browse;
                ScaleTuiAction::Continue
            }
            KeyCode::Char('q') | KeyCode::Char('n') => ScaleTuiAction::Cancel,
            _ => ScaleTuiAction::Continue,
        }
    }

    fn handle_help_key(&mut self, key: KeyEvent) -> ScaleTuiAction {
        match key.code {
            KeyCode::Esc | KeyCode::Enter | KeyCode::Char('?') | KeyCode::Char('q') => {
                self.mode = ScaleTuiMode::Browse;
            }
            _ => {}
        }
        ScaleTuiAction::Continue
    }

    fn selected_row_mut(&mut self) -> Option<&mut RegionRow> {
        let visible = self.visible_indices();
        let selected = *visible.get(self.selected)?;
        self.rows.get_mut(selected)
    }

    fn activate_apply(&mut self) -> ScaleTuiAction {
        if self.changes().is_empty() {
            return ScaleTuiAction::Apply(HashMap::new());
        }

        let total = self.total_desired_replicas();
        if total > MAX_TOTAL_REPLICAS {
            self.error = Some(max_total_error(total));
            return ScaleTuiAction::Continue;
        }

        self.mode = ScaleTuiMode::Confirm;
        ScaleTuiAction::Continue
    }

    fn next_focus(&mut self) {
        self.focus = match self.focus {
            ScaleTuiFocus::Regions => ScaleTuiFocus::Apply,
            ScaleTuiFocus::Apply => ScaleTuiFocus::Cancel,
            ScaleTuiFocus::Cancel => ScaleTuiFocus::Regions,
        };
    }

    fn previous_focus(&mut self) {
        self.focus = match self.focus {
            ScaleTuiFocus::Regions => ScaleTuiFocus::Cancel,
            ScaleTuiFocus::Apply => ScaleTuiFocus::Regions,
            ScaleTuiFocus::Cancel => ScaleTuiFocus::Apply,
        };
    }

    fn move_selection(&mut self, delta: isize) {
        let len = self.visible_indices().len();
        if len == 0 {
            self.selected = 0;
            return;
        }

        let next = self.selected as isize + delta;
        self.selected = next.clamp(0, len.saturating_sub(1) as isize) as usize;
    }

    fn adjust_selected(&mut self, delta: i64) {
        if let Some(row) = self.selected_row() {
            let next = if delta.is_negative() {
                row.desired.saturating_sub(delta.unsigned_abs())
            } else {
                row.desired.saturating_add(delta as u64)
            };
            self.set_selected_desired(next);
        }
    }

    fn set_selected_desired(&mut self, replicas: u64) {
        let max = self.max_for_selected_row();
        let desired = replicas.min(max);
        if replicas > max {
            self.error = Some(format!(
                "Max for this region is {max} replicas ({MAX_TOTAL_REPLICAS} total across regions)."
            ));
        }

        if let Some(row) = self.selected_row_mut() {
            row.desired = desired;
        }
    }

    fn max_for_selected_row(&self) -> u64 {
        let visible = self.visible_indices();
        let Some(selected_idx) = visible.get(self.selected) else {
            return 0;
        };

        let other_replicas = self
            .rows
            .iter()
            .enumerate()
            .filter(|(idx, _)| idx != selected_idx)
            .fold(0u64, |total, (_, row)| total.saturating_add(row.desired));

        MAX_TOTAL_REPLICAS.saturating_sub(other_replicas)
    }
}

fn max_total_error(total: u64) -> String {
    format!(
        "Cannot scale to {total} replicas. Max is {MAX_TOTAL_REPLICAS} total replicas across regions."
    )
}

fn current_replicas(existing: &Value) -> HashMap<String, u64> {
    existing
        .as_object()
        .map(|object| {
            object
                .iter()
                .map(|(name, value)| {
                    let replicas = value
                        .get("numReplicas")
                        .and_then(Value::as_u64)
                        .unwrap_or(0);
                    (name.clone(), replicas)
                })
                .collect()
        })
        .unwrap_or_default()
}

fn shell_arg(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/' | ':'))
    {
        value.to_string()
    } else {
        format!("'{}'", value.replace('\'', "'\\''"))
    }
}

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

    use super::*;

    fn region(
        name: &str,
        location: &str,
        country: &str,
        provider_region: Option<&str>,
        is_deprecated: bool,
    ) -> queries::regions::RegionsRegions {
        queries::regions::RegionsRegions {
            name: name.to_string(),
            region: provider_region.map(ToString::to_string),
            country: country.to_string(),
            location: location.to_string(),
            workspace_id: None,
            deployment_constraints: Some(queries::regions::RegionsRegionsDeploymentConstraints {
                deprecation_info: Some(
                    queries::regions::RegionsRegionsDeploymentConstraintsDeprecationInfo {
                        is_deprecated,
                        replacement_region: "us-west2".to_string(),
                    },
                ),
            }),
        }
    }

    #[test]
    fn app_hides_deprecated_regions_unless_currently_scaled() {
        let regions = queries::regions::ResponseData {
            regions: vec![
                region("us-west2", "US West", "US", Some("us-west2"), false),
                region("old-region", "Old Region", "US", None, true),
            ],
        };

        let app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({"old-region": {"numReplicas": 1}}),
        );

        assert!(app.rows.iter().any(|row| row.name == "old-region"));
        assert!(
            app.rows
                .iter()
                .find(|row| row.name == "old-region")
                .is_some_and(|row| !row.available)
        );
    }

    #[test]
    fn changes_include_only_edited_regions() {
        let regions = queries::regions::ResponseData {
            regions: vec![
                region("us-west2", "US West", "US", Some("us-west2"), false),
                region(
                    "europe-west4-drams3a",
                    "EU West",
                    "NL",
                    Some("europe-west4"),
                    false,
                ),
            ],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({"us-west2": {"numReplicas": 1}}),
        );

        let eu_west = app
            .rows
            .iter_mut()
            .find(|row| row.name == "europe-west4-drams3a")
            .unwrap();
        eu_west.desired = 2;

        assert_eq!(
            app.changes(),
            HashMap::from([("europe-west4-drams3a".to_string(), 2)])
        );
        assert_eq!(
            app.command_preview(),
            "railway scale --environment production --service worker eu-west=2"
        );
    }

    #[test]
    fn command_preview_uses_region_id_when_cli_name_is_ambiguous() {
        let regions = queries::regions::ResponseData {
            regions: vec![
                region("region-a", "EU West", "NL", Some("europe-west4"), false),
                region("region-b", "EU West", "NL", Some("europe-west4"), false),
            ],
        };
        let mut app = ScaleTuiApp::new(
            "web worker".to_string(),
            "production".to_string(),
            regions,
            &json!({}),
        );

        app.rows
            .iter_mut()
            .find(|row| row.name == "region-a")
            .unwrap()
            .desired = 2;

        assert_eq!(
            app.command_preview(),
            "railway scale --environment production --service 'web worker' region-a=2"
        );
    }

    #[test]
    fn typing_digit_starts_inline_edit_without_changing_until_enter() {
        let regions = queries::regions::ResponseData {
            regions: vec![region("us-west2", "US West", "US", Some("us-west2"), false)],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({"us-west2": {"numReplicas": 1}}),
        );

        assert_eq!(
            app.handle_key(KeyEvent::from(KeyCode::Char('4'))),
            ScaleTuiAction::Continue
        );
        assert_eq!(app.mode, ScaleTuiMode::Edit);
        assert_eq!(app.edit_input, "4");
        assert_eq!(app.rows[0].desired, 1);

        assert_eq!(
            app.handle_key(KeyEvent::from(KeyCode::Enter)),
            ScaleTuiAction::Continue
        );
        assert_eq!(app.mode, ScaleTuiMode::Browse);
        assert_eq!(app.rows[0].desired, 4);
    }

    #[test]
    fn typed_replica_count_is_capped_by_remaining_total() {
        let regions = queries::regions::ResponseData {
            regions: vec![
                region("us-west2", "US West", "US", Some("us-west2"), false),
                region(
                    "europe-west4-drams3a",
                    "EU West",
                    "NL",
                    Some("europe-west4"),
                    false,
                ),
            ],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({
                "us-west2": { "numReplicas": 1 },
                "europe-west4-drams3a": { "numReplicas": 1 }
            }),
        );

        let _ = app.handle_key(KeyEvent::from(KeyCode::Char('6')));
        let _ = app.handle_key(KeyEvent::from(KeyCode::Char('0')));
        let _ = app.handle_key(KeyEvent::from(KeyCode::Enter));

        assert_eq!(app.rows[app.selected].desired, 49);
        assert_eq!(app.total_desired_replicas(), MAX_TOTAL_REPLICAS);
        assert!(
            app.error
                .as_deref()
                .is_some_and(|error| error.contains("Max for this region"))
        );
    }

    #[test]
    fn increment_does_not_exceed_total_replica_limit() {
        let regions = queries::regions::ResponseData {
            regions: vec![
                region("us-west2", "US West", "US", Some("us-west2"), false),
                region(
                    "europe-west4-drams3a",
                    "EU West",
                    "NL",
                    Some("europe-west4"),
                    false,
                ),
            ],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({
                "us-west2": { "numReplicas": 49 },
                "europe-west4-drams3a": { "numReplicas": 1 }
            }),
        );

        let _ = app.handle_key(KeyEvent::from(KeyCode::Char('+')));

        assert_eq!(app.total_desired_replicas(), MAX_TOTAL_REPLICAS);
        assert_eq!(app.rows[app.selected].desired, 49);
        assert!(
            app.error
                .as_deref()
                .is_some_and(|error| error.contains("Max for this region"))
        );
    }

    #[test]
    fn escape_cancels_inline_edit() {
        let regions = queries::regions::ResponseData {
            regions: vec![region("us-west2", "US West", "US", Some("us-west2"), false)],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({"us-west2": {"numReplicas": 1}}),
        );

        let _ = app.handle_key(KeyEvent::from(KeyCode::Char('4')));
        let _ = app.handle_key(KeyEvent::from(KeyCode::Esc));

        assert_eq!(app.mode, ScaleTuiMode::Browse);
        assert_eq!(app.edit_input, "");
        assert_eq!(app.rows[0].desired, 1);
    }

    #[test]
    fn tab_cycles_between_regions_and_action_buttons() {
        let regions = queries::regions::ResponseData {
            regions: vec![region("us-west2", "US West", "US", Some("us-west2"), false)],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({}),
        );

        assert_eq!(app.focus, ScaleTuiFocus::Regions);
        let _ = app.handle_key(KeyEvent::from(KeyCode::Tab));
        assert_eq!(app.focus, ScaleTuiFocus::Apply);
        let _ = app.handle_key(KeyEvent::from(KeyCode::Tab));
        assert_eq!(app.focus, ScaleTuiFocus::Cancel);
        let _ = app.handle_key(KeyEvent::from(KeyCode::Tab));
        assert_eq!(app.focus, ScaleTuiFocus::Regions);
    }

    #[test]
    fn action_buttons_activate_from_keyboard() {
        let regions = queries::regions::ResponseData {
            regions: vec![region("us-west2", "US West", "US", Some("us-west2"), false)],
        };
        let mut app = ScaleTuiApp::new(
            "worker".to_string(),
            "production".to_string(),
            regions,
            &json!({"us-west2": {"numReplicas": 1}}),
        );

        app.rows[0].desired = 2;
        let _ = app.handle_key(KeyEvent::from(KeyCode::Tab));
        assert_eq!(app.focus, ScaleTuiFocus::Apply);
        assert_eq!(
            app.handle_key(KeyEvent::from(KeyCode::Enter)),
            ScaleTuiAction::Continue
        );
        assert_eq!(app.mode, ScaleTuiMode::Confirm);

        app.mode = ScaleTuiMode::Browse;
        let _ = app.handle_key(KeyEvent::from(KeyCode::Tab));
        assert_eq!(app.focus, ScaleTuiFocus::Cancel);
        assert_eq!(
            app.handle_key(KeyEvent::from(KeyCode::Enter)),
            ScaleTuiAction::Cancel
        );
    }
}
