use anyhow::bail;
use colored::*;
use is_terminal::IsTerminal;
use serde::Serialize;
use std::{collections::HashSet, fmt::Display};

use crate::{
    controllers::project::{get_environment_instances, get_service_ids_in_env},
    errors::RailwayError,
    util::prompt::{fake_select, prompt_options, prompt_options_skippable},
    workspace::{Project, Workspace, workspaces},
};

use super::*;

/// Associate existing project with current directory, may specify projectId as an argument
#[derive(Parser)]
pub struct Args {
    #[clap(long, short)]
    /// Environment to link to
    environment: Option<String>,

    /// Project to link to
    #[clap(long, short, alias = "project_id")]
    project: Option<String>,

    /// The service to link to
    #[clap(long, short)]
    service: Option<String>,

    /// The team to link to (deprecated: use --workspace instead).
    #[clap(long, short)]
    team: Option<String>,

    /// The workspace to link to.
    #[clap(long, short)]
    workspace: Option<String>,

    /// Output in JSON format
    #[clap(long)]
    json: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LinkOutput {
    project_id: String,
    project_name: String,
    environment_id: String,
    environment_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    service_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    service_name: Option<String>,
}

pub async fn command(args: Args) -> Result<()> {
    let mut configs = Configs::new()?;

    // Support both team (deprecated) and workspace arguments
    let workspace_arg = match (args.team.as_ref(), args.workspace.as_ref()) {
        (Some(_), None) => {
            eprintln!(
                "{}",
                "Warning: The --team flag is deprecated. Please use --workspace instead.".yellow()
            );
            args.team
        }
        (None, workspace) => workspace.cloned(),
        (Some(_), Some(_)) => {
            eprintln!("{}", "Warning: Both --team and --workspace provided. Using --workspace. The --team flag is deprecated.".yellow());
            args.workspace
        }
    };

    let workspaces = workspaces().await?;
    let workspace = select_workspace(args.project.clone(), workspace_arg, workspaces)?;

    let project = select_project(workspace, args.project.clone())?;

    let environment = select_environment(args.environment, &project)?;

    let client = GQLClient::new_authorized(&configs)?;
    let environment_instances =
        get_environment_instances(&client, &configs, &project.id, &environment.id).await?;
    let service_ids = get_service_ids_in_env(&environment_instances);
    let service = select_service(&project, &service_ids, args.service)?;

    configs.link_project(
        project.id.clone(),
        Some(project.name.clone()),
        environment.id.clone(),
        Some(environment.name.clone()),
    )?;

    let (service_id, service_name) = if let Some(ref svc) = service {
        configs.link_service(svc.id.clone())?;
        (Some(svc.id.clone()), Some(svc.name.clone()))
    } else {
        (None, None)
    };

    configs.write()?;

    if args.json {
        let output = LinkOutput {
            project_id: project.id,
            project_name: project.name,
            environment_id: environment.id,
            environment_name: environment.name,
            service_id,
            service_name,
        };
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        println!(
            "\n{} {} {}",
            "Project".green(),
            project.name.magenta().bold(),
            "linked successfully! 🎉".green()
        );
    }

    Ok(())
}

fn select_service(
    project: &NormalisedProject,
    service_ids: &HashSet<String>,
    service: Option<String>,
) -> Result<Option<NormalisedService>, anyhow::Error> {
    let useful_services = project
        .services
        .iter()
        .filter(|&a| service_ids.contains(&a.id))
        .cloned()
        .collect::<Vec<NormalisedService>>();

    let service = if !useful_services.is_empty() {
        if let Some(service) = service {
            let service_norm = useful_services.iter().find(|s| {
                (s.name.to_lowercase() == service.to_lowercase())
                    || (s.id.to_lowercase() == service.to_lowercase())
            });
            if let Some(service) = service_norm {
                fake_select("Select a service", &service.name);
                Some(service.clone())
            } else {
                let available: Vec<&str> = useful_services
                    .iter()
                    .take(5)
                    .map(|s| s.name.as_str())
                    .collect();
                let suffix = if useful_services.len() > 5 {
                    format!(", +{} more", useful_services.len() - 5)
                } else {
                    String::new()
                };
                bail!(
                    "Service \"{}\" not found.\nAvailable: {}{}",
                    service,
                    available.join(", "),
                    suffix
                );
            }
        } else if std::io::stdout().is_terminal() {
            prompt_options_skippable("Select a service <esc to skip>", useful_services)?
        } else if useful_services.len() == 1 {
            let svc = useful_services.into_iter().next().unwrap();
            eprintln!("No service specified — auto-selecting \"{}\"", svc.name);
            Some(svc)
        } else {
            let names: Vec<&str> = useful_services
                .iter()
                .take(5)
                .map(|s| s.name.as_str())
                .collect();
            let suffix = if useful_services.len() > 5 {
                format!(", +{} more", useful_services.len() - 5)
            } else {
                String::new()
            };
            eprintln!(
                "Multiple services available — use --service <name> to link one.\nAvailable: {}{}",
                names.join(", "),
                suffix
            );
            None
        }
    } else {
        None
    };
    Ok(service)
}

fn select_environment(
    environment: Option<String>,
    project: &NormalisedProject,
) -> Result<NormalisedEnvironment, anyhow::Error> {
    if project.environments.is_empty() {
        if !project.restricted_environments.is_empty() {
            bail!("All environments in this project are restricted");
        } else {
            bail!("Project has no environments");
        }
    }

    let environment = if let Some(environment) = environment {
        let env = project.environments.iter().find(|e| {
            (e.name.to_lowercase() == environment.to_lowercase())
                || (e.id.to_lowercase() == environment.to_lowercase())
        });
        if let Some(env) = env {
            fake_select("Select an environment", &env.name);
            env.clone()
        } else if let Some(env) = project.restricted_environments.iter().find(|e| {
            (e.name.to_lowercase() == environment.to_lowercase())
                || (e.id.to_lowercase() == environment.to_lowercase())
        }) {
            bail!(RailwayError::EnvironmentRestricted(env.name.clone()));
        } else {
            let available: Vec<&str> = project
                .environments
                .iter()
                .take(5)
                .map(|e| e.name.as_str())
                .collect();
            let suffix = if project.environments.len() > 5 {
                format!(", +{} more", project.environments.len() - 5)
            } else {
                String::new()
            };
            bail!(
                "Environment \"{}\" not found.\nAvailable: {}{}",
                environment,
                available.join(", "),
                suffix
            );
        }
    } else if project.environments.len() == 1 {
        let env = project.environments[0].clone();
        fake_select("Select an environment", &env.name);
        env
    } else {
        if !std::io::stdout().is_terminal() {
            let names: Vec<&str> = project
                .environments
                .iter()
                .take(5)
                .map(|e| e.name.as_str())
                .collect();
            let suffix = if project.environments.len() > 5 {
                format!(", +{} more", project.environments.len() - 5)
            } else {
                String::new()
            };
            bail!(
                "--environment required in non-interactive mode.\nAvailable: {}{}",
                names.join(", "),
                suffix
            );
        }
        prompt_options("Select an environment", project.environments.clone())?
    };
    Ok(environment)
}

fn select_project(
    workspace: Workspace,
    project: Option<String>,
) -> Result<NormalisedProject, anyhow::Error> {
    let projects = workspace
        .projects()
        .into_iter()
        .filter(|p| p.deleted_at().is_none())
        .collect::<Vec<_>>();

    let project = NormalisedProject::from({
        if let Some(project) = project {
            let proj = projects.iter().find(|pro| {
                (pro.id().to_lowercase() == project.to_lowercase())
                    || (pro.name().to_lowercase() == project.to_lowercase())
            });
            if let Some(project) = proj {
                fake_select("Select a project", &project.to_string());
                project.clone()
            } else {
                let available: Vec<&str> = projects.iter().take(5).map(|p| p.name()).collect();
                let suffix = if projects.len() > 5 {
                    format!(", +{} more", projects.len() - 5)
                } else {
                    String::new()
                };
                bail!(
                    "Project \"{}\" not found in workspace \"{}\".\nAvailable: {}{}",
                    project,
                    workspace.name(),
                    available.join(", "),
                    suffix
                );
            }
        } else {
            prompt_workspace_projects(projects)?
        }
    });
    Ok(project)
}

fn select_workspace(
    project: Option<String>,
    workspace_name: Option<String>,
    workspaces: Vec<Workspace>,
) -> Result<Workspace, anyhow::Error> {
    let workspace = match (project, workspace_name) {
        (Some(project), None) => {
            // It's a project id or name, figure out workspace
            if let Some(workspace) = workspaces.iter().find(|w| {
                w.projects().iter().any(|pro| {
                    pro.id().to_lowercase() == project.to_lowercase()
                        || pro.name().to_lowercase() == project.to_lowercase()
                })
            }) {
                fake_select("Select a workspace", workspace.name());
                workspace.clone()
            } else {
                prompt_workspaces(workspaces)?
            }
        }
        (None, Some(workspace_arg)) | (Some(_), Some(workspace_arg)) => {
            if let Some(workspace) = workspaces.iter().find(|w| {
                w.id().to_lowercase() == workspace_arg.to_lowercase()
                    || w.team_id().map(str::to_lowercase) == Some(workspace_arg.to_lowercase())
                    || w.name().to_lowercase() == workspace_arg.to_lowercase()
            }) {
                fake_select("Select a workspace", workspace.name());
                workspace.clone()
            } else if workspace_arg.to_lowercase() == "personal" {
                bail!(RailwayError::NoPersonalWorkspace);
            } else {
                return Err(RailwayError::WorkspaceNotFound(workspace_arg.clone()).into());
            }
        }
        (None, None) => prompt_workspaces(workspaces)?,
    };
    Ok(workspace)
}

fn prompt_workspaces(workspaces: Vec<Workspace>) -> Result<Workspace> {
    if workspaces.is_empty() {
        return Err(RailwayError::NoProjects.into());
    }
    if workspaces.len() == 1 {
        fake_select("Select a workspace", workspaces[0].name());
        return Ok(workspaces[0].clone());
    }
    if !std::io::stdout().is_terminal() {
        let names: Vec<&str> = workspaces.iter().take(5).map(|w| w.name()).collect();
        let suffix = if workspaces.len() > 5 {
            format!(", +{} more", workspaces.len() - 5)
        } else {
            String::new()
        };
        bail!(
            "--workspace required in non-interactive mode.\nAvailable: {}{}",
            names.join(", "),
            suffix
        );
    }
    prompt_options("Select a workspace", workspaces)
}

fn prompt_workspace_projects(projects: Vec<Project>) -> Result<Project, anyhow::Error> {
    if !std::io::stdout().is_terminal() {
        let names: Vec<String> = projects
            .iter()
            .take(5)
            .map(|p| p.name().to_owned())
            .collect();
        let suffix = if projects.len() > 5 {
            format!(", +{} more", projects.len() - 5)
        } else {
            String::new()
        };
        bail!(
            "--project required in non-interactive mode.\nAvailable: {}{}",
            names.join(", "),
            suffix
        );
    }
    prompt_options("Select a project", projects)
}

structstruck::strike! {
    #[strikethrough[derive(Debug, Clone, derive_new::new)]]
    struct NormalisedProject {
        /// Project ID
        id: String,
        /// Project name
        name: String,
        /// Project environments
        environments: Vec<struct NormalisedEnvironment {
            /// Environment ID
            id: String,
            /// Environment Name
            name: String
        }>,
        /// Project environments the current user cannot access
        restricted_environments: Vec<NormalisedEnvironment>,
        /// Project services
        services: Vec<struct NormalisedService {
            /// Service ID
            id: String,
            /// Service name
            name: String,
        }>,
    }
}

impl From<Project> for NormalisedProject {
    fn from(value: Project) -> Self {
        match value {
            Project::External(project) => {
                let mut accessible_envs = Vec::new();
                let mut restricted_envs = Vec::new();
                for env in project.environments.edges {
                    let normalised = NormalisedEnvironment::new(env.node.id, env.node.name);
                    if env.node.can_access {
                        accessible_envs.push(normalised);
                    } else {
                        restricted_envs.push(normalised);
                    }
                }
                NormalisedProject::new(
                    project.id,
                    project.name,
                    accessible_envs,
                    restricted_envs,
                    project
                        .services
                        .edges
                        .into_iter()
                        .map(|service| NormalisedService::new(service.node.id, service.node.name))
                        .collect(),
                )
            }
            Project::Workspace(project) => {
                let mut accessible_envs = Vec::new();
                let mut restricted_envs = Vec::new();
                for env in project.environments.edges {
                    let normalised = NormalisedEnvironment::new(env.node.id, env.node.name);
                    if env.node.can_access {
                        accessible_envs.push(normalised);
                    } else {
                        restricted_envs.push(normalised);
                    }
                }
                NormalisedProject::new(
                    project.id,
                    project.name,
                    accessible_envs,
                    restricted_envs,
                    project
                        .services
                        .edges
                        .into_iter()
                        .map(|service| NormalisedService::new(service.node.id, service.node.name))
                        .collect(),
                )
            }
        }
    }
}

impl Display for NormalisedEnvironment {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name)
    }
}

impl Display for NormalisedService {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name)
    }
}
