use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use crate::app::ProviderFormBaseline;
use crate::app::forms::ProviderFormFields;
use crate::providers::config::{ProviderConfig, ProviderConfigId};

/// Record of the last sync result for a provider.
#[derive(Debug, Clone)]
pub struct SyncRecord {
    pub timestamp: u64,
    pub message: String,
    pub is_error: bool,
}

impl SyncRecord {
    /// Load sync history from ~/.purple/sync_history.tsv.
    /// Format: provider\ttimestamp\tis_error\tmessage
    pub fn load_all() -> HashMap<String, SyncRecord> {
        let mut map = HashMap::new();
        let Some(home) = dirs::home_dir() else {
            return map;
        };
        let path = home.join(".purple").join("sync_history.tsv");
        let Ok(content) = std::fs::read_to_string(&path) else {
            return map;
        };
        for line in content.lines() {
            let parts: Vec<&str> = line.splitn(4, '\t').collect();
            if parts.len() < 4 {
                continue;
            }
            let Some(ts) = parts[1].parse::<u64>().ok() else {
                continue;
            };
            let is_error = parts[2] == "1";
            map.insert(
                parts[0].to_string(),
                SyncRecord {
                    timestamp: ts,
                    message: parts[3].to_string(),
                    is_error,
                },
            );
        }
        map
    }

    /// Save sync history to ~/.purple/sync_history.tsv.
    pub fn save_all(history: &HashMap<String, SyncRecord>) {
        if crate::demo_flag::is_demo() {
            return;
        }
        let Some(home) = dirs::home_dir() else { return };
        let dir = home.join(".purple");
        let path = dir.join("sync_history.tsv");
        let mut lines = Vec::new();
        for (provider, record) in history {
            lines.push(format!(
                "{}\t{}\t{}\t{}",
                provider,
                record.timestamp,
                if record.is_error { "1" } else { "0" },
                record.message
            ));
        }
        if let Err(e) = crate::fs_util::atomic_write(&path, lines.join("\n").as_bytes()) {
            log::warn!(
                "[config] failed to save sync_history.tsv at {}: {e}",
                path.display()
            );
        }
    }

    /// Parse sync history from TSV content string (for demo/test use).
    pub fn load_from_content(content: &str) -> HashMap<String, SyncRecord> {
        let mut map = HashMap::new();
        for line in content.lines() {
            let parts: Vec<&str> = line.splitn(4, '\t').collect();
            if parts.len() < 4 {
                continue;
            }
            let Some(ts) = parts[1].parse::<u64>().ok() else {
                continue;
            };
            let is_error = parts[2] == "1";
            map.insert(
                parts[0].to_string(),
                SyncRecord {
                    timestamp: ts,
                    message: parts[3].to_string(),
                    is_error,
                },
            );
        }
        map
    }
}

/// Provider-owned state grouped off the `App` god-struct. Holds the
/// provider config, the edit form, the in-flight sync tracking
/// (cancel flags, completed names, error aggregate), the pending
/// delete alias, the on-disk sync history and the dirty-check baseline.
/// Pure state container.
pub struct ProviderState {
    pub config: ProviderConfig,
    pub form: ProviderFormFields,
    pub syncing: HashMap<String, Arc<AtomicBool>>,
    /// Names of providers that completed during this sync batch.
    pub sync_done: Vec<String>,
    /// Whether any provider in the current batch had errors.
    pub sync_had_errors: bool,
    /// Aggregate diff counts across the current sync batch. Reset when the
    /// batch finishes (no providers left in `syncing`). Used by the footer
    /// background status to render `(+3 ~1 -2)` next to the provider list.
    pub batch_added: usize,
    pub batch_updated: usize,
    pub batch_stale: usize,
    /// Total provider count for the current batch (done + still syncing).
    /// Captured when sync starts so the `n/total` counter does not jump
    /// when providers complete and leave `syncing`.
    pub batch_total: usize,
    pub pending_delete: Option<String>,
    /// When deleting a single labeled config, this carries the full id.
    /// `pending_delete` is used for whole-provider delete (header confirm).
    pub pending_delete_id: Option<ProviderConfigId>,
    pub sync_history: HashMap<String, SyncRecord>,
    pub form_baseline: Option<ProviderFormBaseline>,
    /// Provider names that are expanded in the tree-style provider list.
    /// Only matters when a provider has 2+ labeled configs.
    pub expanded_providers: HashSet<String>,
    /// In-progress lazy migration: when adding a 2nd config of a provider
    /// that currently has a single bare config, we first prompt for a label
    /// for the existing config. The chosen label lives here until the new
    /// config form is saved (then both writes happen atomically). When the
    /// user cancels the new config form, this is dropped and nothing is
    /// written.
    pub pending_label_migration: Option<PendingLabelMigration>,
}

/// State carried between step 1 (label both configs) and step 2
/// (fill in the new labeled config form) of the lazy-migration add flow.
#[derive(Debug, Clone)]
pub struct PendingLabelMigration {
    pub provider: String,
    /// User-chosen label for the EXISTING (currently bare) config.
    pub existing_label: String,
    /// User-chosen label for the NEW config being added.
    pub new_label: String,
    /// Which field has focus in the label-migration screen.
    pub focused: LabelMigrationField,
    /// Cursor position (char index) within the focused field's value.
    pub cursor_pos: usize,
}

impl PendingLabelMigration {
    /// Get the focused field's value.
    pub fn focused_value(&self) -> &str {
        match self.focused {
            LabelMigrationField::Existing => &self.existing_label,
            LabelMigrationField::New => &self.new_label,
        }
    }

    /// Get the focused field's value mutably.
    pub fn focused_value_mut(&mut self) -> &mut String {
        match self.focused {
            LabelMigrationField::Existing => &mut self.existing_label,
            LabelMigrationField::New => &mut self.new_label,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LabelMigrationField {
    Existing,
    New,
}

/// One row in the tree-style provider list.
#[derive(Debug, Clone)]
pub enum ProviderRow {
    /// Provider group header. `config_count` 0 = unconfigured, 1 = single
    /// config (bare or labeled), 2+ = group that can be expanded.
    Header { name: String, config_count: usize },
    /// One labeled config under an expanded header.
    Leaf { id: ProviderConfigId },
}

impl ProviderRow {
    pub fn provider_name(&self) -> &str {
        match self {
            ProviderRow::Header { name, .. } => name,
            ProviderRow::Leaf { id } => &id.provider,
        }
    }
}

impl ProviderState {
    /// Reset batch counters when a completely new sync run begins.
    ///
    /// Call before inserting into `syncing` on every spawn path. When both
    /// `syncing` and `sync_done` are empty a fresh batch is starting, so
    /// stale `batch_total` / `batch_added` / `batch_updated` / `batch_stale`
    /// values from a previous (non-completed) run are cleared. Without this
    /// guard a rare edge case could leak state from an interrupted batch
    /// into a smaller follow-up batch and show "Syncing 1/5" while only
    /// one provider is actually in flight.
    pub fn reset_batch_if_idle(&mut self) {
        if self.syncing.is_empty() && self.sync_done.is_empty() {
            self.batch_total = 0;
            self.batch_added = 0;
            self.batch_updated = 0;
            self.batch_stale = 0;
            self.sync_had_errors = false;
        }
    }
}

impl Default for ProviderState {
    /// Truly empty default. No disk I/O. Call sites that need persisted
    /// state (App::new) construct with struct-update syntax:
    /// `ProviderState { config: ProviderConfig::load(), sync_history: SyncRecord::load_all(), ..Default::default() }`.
    fn default() -> Self {
        Self {
            config: ProviderConfig::default(),
            form: ProviderFormFields::new(),
            syncing: HashMap::new(),
            sync_done: Vec::new(),
            sync_had_errors: false,
            batch_added: 0,
            batch_updated: 0,
            batch_stale: 0,
            batch_total: 0,
            pending_delete: None,
            pending_delete_id: None,
            sync_history: HashMap::new(),
            form_baseline: None,
            expanded_providers: HashSet::new(),
            pending_label_migration: None,
        }
    }
}

impl ProviderState {
    /// Construct with persisted state loaded from disk.
    pub fn load() -> Self {
        Self {
            config: crate::providers::config::ProviderConfig::load(),
            sync_history: SyncRecord::load_all(),
            ..Self::default()
        }
    }

    /// One row in the provider list, in display order.
    /// Each provider is a `Header`. When the provider has 2+ labeled configs
    /// AND is in `expanded_providers`, its `Leaf` rows follow immediately.
    /// When the provider has 0 or 1 config, no leaves are emitted.
    pub fn provider_list_rows(&self) -> Vec<ProviderRow> {
        let mut rows = Vec::new();
        for name in self.sorted_names() {
            let configs = self.config.sections_for_provider(&name);
            rows.push(ProviderRow::Header {
                name: name.clone(),
                config_count: configs.len(),
            });
            if configs.len() >= 2 && self.expanded_providers.contains(&name) {
                let mut sorted = configs.clone();
                sorted.sort_by(|a, b| {
                    a.id.label
                        .as_deref()
                        .unwrap_or("")
                        .cmp(b.id.label.as_deref().unwrap_or(""))
                });
                for s in sorted {
                    rows.push(ProviderRow::Leaf { id: s.id.clone() });
                }
            }
        }
        rows
    }

    /// Provider names sorted by last sync (most recent first), then configured,
    /// then unconfigured. Includes any unknown provider names found in the
    /// config file (e.g. typos or future providers).
    pub fn sorted_names(&self) -> Vec<String> {
        use crate::providers;
        let mut names: Vec<String> = providers::PROVIDER_NAMES
            .iter()
            .map(|s| s.to_string())
            .collect();
        // Append configured providers not in the known list so they are visible and removable
        for section in &self.config.sections {
            let name = section.provider().to_string();
            if !names.contains(&name) {
                names.push(name);
            }
        }
        // For multi-config providers the sync_history keys are the full id
        // ("digitalocean:work"), not the bare name. Take the MAX timestamp
        // across any history entry whose key matches this provider so the
        // recency sort works for both single and multi-config layouts.
        let max_ts = |provider: &str| -> u64 {
            self.sync_history
                .iter()
                .filter(|(k, _)| {
                    k.as_str() == provider || k.split_once(':').is_some_and(|(p, _)| p == provider)
                })
                .map(|(_, r)| r.timestamp)
                .max()
                .unwrap_or(0)
        };
        names.sort_by(|a, b| {
            let conf_a = self.config.section(a.as_str()).is_some();
            let conf_b = self.config.section(b.as_str()).is_some();
            let ts_a = max_ts(a.as_str());
            let ts_b = max_ts(b.as_str());
            // Configured first (by most recent sync), then unconfigured alphabetically
            conf_b.cmp(&conf_a).then(ts_b.cmp(&ts_a)).then(a.cmp(b))
        });
        names
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_is_empty() {
        // Must not touch disk. Constructed with ProviderConfig::default()
        // and an empty sync_history. App::new() layers the real on-disk
        // state on top via struct-update syntax.
        let s = ProviderState::default();
        assert!(s.config.sections.is_empty());
        assert!(s.config.path_override.is_none());
        assert!(s.syncing.is_empty());
        assert!(s.sync_done.is_empty());
        assert!(!s.sync_had_errors);
        assert!(s.pending_delete.is_none());
        assert!(s.sync_history.is_empty());
        assert!(s.form_baseline.is_none());
    }

    #[test]
    fn sorted_names_returns_configured_providers_before_unconfigured() {
        use crate::providers::config::ProviderSection;

        let mut state = ProviderState::default();
        state.config.sections.push(ProviderSection {
            id: crate::providers::config::ProviderConfigId::bare("vultr"),
            token: "tok".to_string(),
            alias_prefix: "vultr".to_string(),
            ..ProviderSection::default()
        });
        state.config.sections.push(ProviderSection {
            id: crate::providers::config::ProviderConfigId::bare("digitalocean"),
            token: "tok".to_string(),
            alias_prefix: "do".to_string(),
            ..ProviderSection::default()
        });
        state.sync_history.insert(
            "digitalocean".to_string(),
            SyncRecord {
                timestamp: 2_000,
                message: "ok".to_string(),
                is_error: false,
            },
        );
        state.sync_history.insert(
            "vultr".to_string(),
            SyncRecord {
                timestamp: 1_000,
                message: "ok".to_string(),
                is_error: false,
            },
        );

        let names = state.sorted_names();
        // Configured providers (most recent sync first) precede unconfigured.
        assert_eq!(&names[0], "digitalocean");
        assert_eq!(&names[1], "vultr");
        // Every known provider name must be present.
        for &known in crate::providers::PROVIDER_NAMES {
            assert!(names.iter().any(|n| n == known), "missing {}", known);
        }
        // Unconfigured tail is sorted alphabetically.
        let unconfigured: Vec<&String> = names.iter().skip(2).collect();
        let mut sorted = unconfigured.clone();
        sorted.sort();
        assert_eq!(unconfigured, sorted);
    }

    #[test]
    fn sorted_names_includes_unknown_providers_from_config() {
        use crate::providers::config::ProviderSection;

        let mut state = ProviderState::default();
        state.config.sections.push(ProviderSection {
            id: crate::providers::config::ProviderConfigId::bare("someday_provider"),
            token: "tok".to_string(),
            alias_prefix: "x".to_string(),
            ..ProviderSection::default()
        });

        let names = state.sorted_names();
        assert!(names.iter().any(|n| n == "someday_provider"));
    }
}
