use std::collections::HashMap;
use std::path::PathBuf;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use super::identifiers::{ApiToken, ProjectSlug, WorkspaceSlug};

// --- Private raw TOML deserialization types (not pub, no hook enforcement) ---

#[derive(Deserialize, Serialize)]
struct PlaneSettingsRaw {
    active_workspace: Option<String>,
    default_project: Option<String>,
    #[serde(default)]
    workspaces: HashMap<String, WorkspaceEntryRaw>,
}

#[derive(Deserialize, Serialize)]
struct WorkspaceEntryRaw {
    api_key: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    default_project: Option<String>,
}

// --- Public validated types ---

/// Which workspace the CLI targets by default, resolved from config.
/// Project scope lives on each workspace entry, not here.
pub enum DefaultTarget {
    /// No workspace configured yet — user must run `plane config` before workspace commands
    Unconfigured,
    /// A workspace is set — project scope comes from the workspace entry
    WorkspaceOnly(WorkspaceSlug),
}

/// Whether a workspace has a default project configured
#[derive(Debug, Clone)]
pub enum WorkspaceProjectScope {
    /// No default project — commands require explicit --project
    Unscoped,
    /// A default project is set for this workspace
    Scoped(ProjectSlug),
}

/// A single workspace entry from the config file, holding the API credentials
/// and optional default project for that workspace.
pub struct PlaneWorkspaceEntry {
    api_token: ApiToken,
    project_scope: WorkspaceProjectScope,
}

/// Construction and field access for workspace credential entries
impl PlaneWorkspaceEntry {
    /// Creates an entry with credentials only; project scope defaults to Unscoped
    pub fn new(api_token: ApiToken) -> Self {
        Self {
            api_token,
            project_scope: WorkspaceProjectScope::Unscoped,
        }
    }

    /// Sets the default project scope on this workspace entry, consuming and returning self
    pub fn with_project(mut self, project_scope: WorkspaceProjectScope) -> Self {
        self.project_scope = project_scope;
        self
    }

    /// Returns a reference to the API token for building authenticated requests
    pub fn api_token(&self) -> &ApiToken {
        &self.api_token
    }

    /// Returns the workspace's default project scope
    pub fn project_scope(&self) -> &WorkspaceProjectScope {
        &self.project_scope
    }

    /// Consumes the entry and returns the owned API token,
    /// avoiding clones when the entry is no longer needed after extraction
    pub fn into_api_token(self) -> ApiToken {
        self.api_token
    }

    /// Consumes the entry and returns both token and project scope
    pub fn into_parts(self) -> (ApiToken, WorkspaceProjectScope) {
        (self.api_token, self.project_scope)
    }
}

/// Typed collection of configured Plane workspaces keyed by slug.
/// Enforces that all lookups go through validated `WorkspaceSlug` keys
/// rather than raw strings.
pub struct WorkspaceRegistry(HashMap<WorkspaceSlug, PlaneWorkspaceEntry>);

/// Construction, lookup, and iteration for the workspace collection
impl WorkspaceRegistry {
    /// Wraps a pre-built map of workspace entries into the typed registry
    pub fn from_entries(workspace_entries: HashMap<WorkspaceSlug, PlaneWorkspaceEntry>) -> Self {
        Self(workspace_entries)
    }

    /// Creates an empty registry for first-run config setup
    pub fn empty() -> Self {
        Self(HashMap::new())
    }

    /// Looks up a workspace by slug, returning None if the slug is not configured
    pub fn get(&self, workspace_slug: &WorkspaceSlug) -> Option<&PlaneWorkspaceEntry> {
        self.0.get(workspace_slug)
    }

    /// Removes and returns a workspace entry by slug, transferring ownership
    /// to the caller without cloning. Returns None if the slug is not in the registry.
    pub fn remove_workspace(
        &mut self,
        workspace_slug: &WorkspaceSlug,
    ) -> Option<PlaneWorkspaceEntry> {
        self.0.remove(workspace_slug)
    }

    /// Iterates over all configured workspace slugs for cross-workspace operations
    pub fn slugs(&self) -> impl Iterator<Item = &WorkspaceSlug> {
        self.0.keys()
    }

    /// Consumes the registry and returns the inner map for external mutation
    /// (e.g. adding or removing workspaces) before reconstructing via `from_entries`
    pub fn into_workspace_entries(self) -> HashMap<WorkspaceSlug, PlaneWorkspaceEntry> {
        self.0
    }

    /// Returns the number of configured workspaces for display in config summary
    pub fn workspace_count(&self) -> usize {
        self.0.len()
    }

    /// Returns (slug, entry) pairs for serialization back to TOML
    fn iter(&self) -> impl Iterator<Item = (&WorkspaceSlug, &PlaneWorkspaceEntry)> {
        self.0.iter()
    }
}

/// Persisted CLI settings loaded from ~/.plane-cli-requiem/config.toml.
/// Stores workspace credentials and default targeting preferences.
/// Use `load_plane_settings` / `save_plane_settings` for disk I/O.
pub struct PlaneSettings {
    default_target: DefaultTarget,
    workspaces: WorkspaceRegistry,
}

/// Construction, access, and conversion for the persisted settings
impl PlaneSettings {
    /// Creates empty settings for first-run when no config file exists on disk
    pub fn empty() -> Self {
        Self {
            default_target: DefaultTarget::Unconfigured,
            workspaces: WorkspaceRegistry::empty(),
        }
    }

    /// Returns the configured default workspace and project targeting
    pub fn default_target(&self) -> &DefaultTarget {
        &self.default_target
    }

    /// Returns the registry of all configured workspaces for lookup operations
    pub fn workspaces(&self) -> &WorkspaceRegistry {
        &self.workspaces
    }

    /// Destructures into owned parts for consuming operations like session resolution,
    /// avoiding clones by transferring ownership of the target and registry
    pub fn into_parts(self) -> (DefaultTarget, WorkspaceRegistry) {
        (self.default_target, self.workspaces)
    }

    /// Starts building settings with the specified default targeting.
    /// Chain with `with_workspaces()` to attach workspace credentials before saving.
    pub fn with_target(default_target: DefaultTarget) -> Self {
        Self {
            default_target,
            workspaces: WorkspaceRegistry::empty(),
        }
    }

    /// Attaches a workspace registry to the settings being built.
    /// Consumes self to enable fluent builder chaining before passing to `save_plane_settings`.
    pub fn with_workspaces(mut self, workspace_registry: WorkspaceRegistry) -> Self {
        self.workspaces = workspace_registry;
        self
    }

    fn try_from_raw(raw_settings: PlaneSettingsRaw) -> Result<Self> {
        let legacy_global_project = raw_settings.default_project;

        let default_target = match raw_settings.active_workspace {
            None => DefaultTarget::Unconfigured,
            Some(workspace_text) => DefaultTarget::WorkspaceOnly(workspace_text.parse()?),
        };

        let active_slug_text = match &default_target {
            DefaultTarget::Unconfigured => None,
            DefaultTarget::WorkspaceOnly(active_slug) => Some(active_slug.to_string()),
        };

        let mut workspace_entries = HashMap::new();
        for (slug_text, raw_entry) in raw_settings.workspaces {
            let validated_slug: WorkspaceSlug = slug_text.parse()?;
            let validated_token: ApiToken = raw_entry.api_key.parse()?;

            let project_scope = match raw_entry.default_project {
                Some(project_text) => WorkspaceProjectScope::Scoped(project_text.parse()?),
                None => {
                    let is_active_workspace = active_slug_text
                        .as_ref()
                        .map_or(false, |active| active == &validated_slug.to_string());
                    match (&legacy_global_project, is_active_workspace) {
                        (Some(legacy_project), true) => {
                            WorkspaceProjectScope::Scoped(legacy_project.parse()?)
                        }
                        _ => WorkspaceProjectScope::Unscoped,
                    }
                }
            };

            workspace_entries.insert(
                validated_slug,
                PlaneWorkspaceEntry::new(validated_token).with_project(project_scope),
            );
        }

        Ok(Self {
            default_target,
            workspaces: WorkspaceRegistry::from_entries(workspace_entries),
        })
    }

    fn to_raw(&self) -> PlaneSettingsRaw {
        let active_workspace = match &self.default_target {
            DefaultTarget::Unconfigured => None,
            DefaultTarget::WorkspaceOnly(workspace_slug) => Some(workspace_slug.to_string()),
        };

        let workspaces = self
            .workspaces
            .iter()
            .map(|(workspace_slug, workspace_entry)| {
                let raw_project = match workspace_entry.project_scope() {
                    WorkspaceProjectScope::Unscoped => None,
                    WorkspaceProjectScope::Scoped(project_slug) => {
                        Some(project_slug.to_string())
                    }
                };
                (
                    workspace_slug.to_string(),
                    WorkspaceEntryRaw {
                        api_key: workspace_entry.api_token().as_str().to_owned(),
                        default_project: raw_project,
                    },
                )
            })
            .collect();

        PlaneSettingsRaw {
            active_workspace,
            default_project: None,
            workspaces,
        }
    }
}

fn settings_directory_path() -> Result<PathBuf> {
    let home_directory = dirs::home_dir().context("could not find home directory")?;
    Ok(home_directory.join(".plane-cli-requiem"))
}

fn settings_file_path() -> Result<PathBuf> {
    Ok(settings_directory_path()?.join("config.toml"))
}

/// Reads ~/.plane-cli-requiem/config.toml and parses it into validated settings.
/// Returns empty defaults when the config file does not exist yet (first run).
pub fn load_plane_settings() -> Result<PlaneSettings> {
    let config_file_path = settings_file_path()?;
    if !config_file_path.exists() {
        return Ok(PlaneSettings::empty());
    }
    let toml_content = std::fs::read_to_string(&config_file_path)
        .with_context(|| format!("failed to read {}", config_file_path.display()))?;
    let raw_settings: PlaneSettingsRaw = toml::from_str(&toml_content)
        .with_context(|| format!("failed to parse {}", config_file_path.display()))?;
    PlaneSettings::try_from_raw(raw_settings)
}

/// Serializes settings back to TOML and writes to ~/.plane-cli-requiem/config.toml,
/// creating the .plane-cli-requiem directory if it doesn't exist yet.
pub fn save_plane_settings(plane_settings: &PlaneSettings) -> Result<()> {
    let settings_dir = settings_directory_path()?;
    std::fs::create_dir_all(&settings_dir)?;
    let config_file_path = settings_file_path()?;
    let raw_settings = plane_settings.to_raw();
    let toml_output = toml::to_string_pretty(&raw_settings)?;
    std::fs::write(&config_file_path, toml_output)?;
    Ok(())
}
