use std::{
    collections::{HashMap, HashSet},
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result, anyhow, bail};
use indexmap::IndexMap;
use jsonc_parser::{ParseOptions, parse_to_serde_value};
use serde::Deserialize;
use wildmatch::WildMatch;

use crate::core::{
    project::node_modules_bin_dirs,
    types::{NativeDenoTaskExecution, NativeDenoTaskStage, NativeDenoTaskStep},
};

#[derive(Debug, Clone)]
pub(crate) struct DenoProject {
    pub root: PathBuf,
    pub deno_tasks: IndexMap<String, DenoTaskDefinition>,
    pub has_workspace: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DenoTaskDefinition {
    pub command: Option<String>,
    pub description: Option<String>,
    pub dependencies: Vec<String>,
}

pub(crate) fn find_nearest_deno_project(cwd: &Path) -> Result<Option<DenoProject>> {
    for dir in cwd.ancestors() {
        let Some(config) = read_deno_config(dir)? else {
            continue;
        };

        return Ok(Some(DenoProject {
            root: dir.to_path_buf(),
            deno_tasks: config.tasks,
            has_workspace: config.has_workspace,
        }));
    }

    Ok(None)
}

pub(crate) fn plan_native_deno_task(
    project: &DenoProject,
    selection: &str,
    forwarded_args: &[String],
    has_if_present: bool,
) -> Result<NativeDenoTaskExecution> {
    if project.has_workspace {
        bail!("deno workspace task execution stays in package-manager mode");
    }

    let deno_matches = match_deno_tasks(&project.deno_tasks, selection);
    if !deno_matches.is_empty() {
        let stages = build_deno_task_stages(&project.deno_tasks, &deno_matches, forwarded_args)?;
        return Ok(NativeDenoTaskExecution {
            project_root: project.root.clone(),
            selection: selection.to_string(),
            stages,
            forwarded_args: forwarded_args.to_vec(),
            bin_paths: node_modules_bin_dirs(&project.root),
        });
    }

    if has_if_present {
        return Ok(NativeDenoTaskExecution {
            project_root: project.root.clone(),
            selection: selection.to_string(),
            stages: Vec::new(),
            forwarded_args: forwarded_args.to_vec(),
            bin_paths: node_modules_bin_dirs(&project.root),
        });
    }

    bail!("task '{selection}' was not found in the nearest deno project")
}

fn build_deno_task_stages(
    tasks: &IndexMap<String, DenoTaskDefinition>,
    roots: &[String],
    forwarded_args: &[String],
) -> Result<Vec<NativeDenoTaskStage>> {
    #[derive(Clone, Copy, PartialEq, Eq)]
    enum VisitState {
        Visiting,
        Visited,
    }

    let positions = tasks
        .keys()
        .enumerate()
        .map(|(idx, name)| (name.clone(), idx))
        .collect::<HashMap<_, _>>();

    let mut visit_states = HashMap::<String, VisitState>::new();
    let mut selected = HashSet::<String>::new();
    let mut indegree = HashMap::<String, usize>::new();
    let mut outgoing = HashMap::<String, Vec<String>>::new();

    fn resolve_pattern(
        tasks: &IndexMap<String, DenoTaskDefinition>,
        pattern: &str,
    ) -> Result<Vec<String>> {
        if pattern.contains('*') {
            let matches = match_deno_tasks(tasks, pattern);
            if matches.is_empty() {
                bail!("task pattern '{pattern}' matched no deno tasks");
            }
            return Ok(matches);
        }

        if !tasks.contains_key(pattern) {
            bail!("task '{pattern}' was not found in deno.json");
        }

        Ok(vec![pattern.to_string()])
    }

    fn visit_task(
        task_name: &str,
        tasks: &IndexMap<String, DenoTaskDefinition>,
        visit_states: &mut HashMap<String, VisitState>,
        selected: &mut HashSet<String>,
        indegree: &mut HashMap<String, usize>,
        outgoing: &mut HashMap<String, Vec<String>>,
        stack: &mut Vec<String>,
    ) -> Result<()> {
        if let Some(state) = visit_states.get(task_name) {
            return match state {
                VisitState::Visited => Ok(()),
                VisitState::Visiting => {
                    let mut cycle = stack.clone();
                    cycle.push(task_name.to_string());
                    bail!("task cycle detected: {}", cycle.join(" -> "))
                }
            };
        }

        let task = tasks
            .get(task_name)
            .ok_or_else(|| anyhow!("task '{task_name}' was not found in deno.json"))?;
        visit_states.insert(task_name.to_string(), VisitState::Visiting);
        selected.insert(task_name.to_string());
        indegree.entry(task_name.to_string()).or_insert(0);
        stack.push(task_name.to_string());

        for dependency in &task.dependencies {
            for matched in resolve_pattern(tasks, dependency)? {
                visit_task(
                    &matched,
                    tasks,
                    visit_states,
                    selected,
                    indegree,
                    outgoing,
                    stack,
                )?;
                let entry = outgoing.entry(matched.clone()).or_default();
                if !entry.contains(&task_name.to_string()) {
                    entry.push(task_name.to_string());
                    *indegree.entry(task_name.to_string()).or_insert(0) += 1;
                }
            }
        }

        stack.pop();
        visit_states.insert(task_name.to_string(), VisitState::Visited);
        Ok(())
    }

    let mut root_set = HashSet::<String>::new();
    for root in roots {
        for matched in resolve_pattern(tasks, root)? {
            root_set.insert(matched.clone());
            visit_task(
                &matched,
                tasks,
                &mut visit_states,
                &mut selected,
                &mut indegree,
                &mut outgoing,
                &mut Vec::new(),
            )?;
        }
    }

    let mut completed = HashSet::<String>::new();
    let mut stages = Vec::new();

    while completed.len() < selected.len() {
        let mut ready = selected
            .iter()
            .filter(|name| !completed.contains(*name))
            .filter(|name| indegree.get(*name).copied().unwrap_or_default() == 0)
            .cloned()
            .collect::<Vec<_>>();
        ready.sort_by_key(|name| positions.get(name).copied().unwrap_or(usize::MAX));

        if ready.is_empty() {
            bail!("task cycle detected");
        }

        let mut steps = Vec::new();
        for name in &ready {
            if let Some(task) = tasks.get(name)
                && let Some(command) = &task.command
            {
                let forward_args = !forwarded_args.is_empty() && root_set.contains(name);
                steps.push(NativeDenoTaskStep {
                    task_name: name.clone(),
                    command: command.clone(),
                    forward_args,
                });
            }
        }

        if !steps.is_empty() {
            stages.push(NativeDenoTaskStage { steps });
        }

        for name in ready {
            completed.insert(name.clone());
            if let Some(children) = outgoing.get(&name) {
                for child in children {
                    if let Some(value) = indegree.get_mut(child) {
                        *value -= 1;
                    }
                }
            }
        }
    }

    Ok(stages)
}

fn match_deno_tasks(tasks: &IndexMap<String, DenoTaskDefinition>, pattern: &str) -> Vec<String> {
    if !pattern.contains(['*', '?']) {
        return if tasks.contains_key(pattern) {
            vec![pattern.to_string()]
        } else {
            Vec::new()
        };
    }

    let matcher = WildMatch::new(pattern);
    tasks
        .keys()
        .filter(|name| matcher.matches(name))
        .cloned()
        .collect()
}

#[cfg(test)]
fn wildcard_matches(pattern: &str, value: &str) -> bool {
    WildMatch::new(pattern).matches(value)
}

#[derive(Debug, Clone)]
struct ParsedDenoConfig {
    tasks: IndexMap<String, DenoTaskDefinition>,
    has_workspace: bool,
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
struct RawDenoConfig {
    tasks: IndexMap<String, RawDenoTask>,
    workspace: Option<Vec<String>>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum RawDenoTask {
    Command(String),
    Detail {
        command: Option<String>,
        description: Option<String>,
        #[serde(default)]
        dependencies: Vec<String>,
    },
}

fn read_deno_config(dir: &Path) -> Result<Option<ParsedDenoConfig>> {
    let path = if dir.join("deno.json").is_file() {
        dir.join("deno.json")
    } else if dir.join("deno.jsonc").is_file() {
        dir.join("deno.jsonc")
    } else {
        return Ok(None);
    };

    let raw = fs::read_to_string(&path)
        .with_context(|| format!("config error: failed to read {}", path.display()))?;
    let config = parse_to_serde_value::<RawDenoConfig>(&raw, &ParseOptions::default())
        .map_err(anyhow::Error::msg)
        .with_context(|| format!("config error: failed to parse {}", path.display()))?;
    let tasks = config
        .tasks
        .into_iter()
        .map(|(name, task)| {
            let normalized = match task {
                RawDenoTask::Command(command) => DenoTaskDefinition {
                    command: Some(command),
                    description: None,
                    dependencies: Vec::new(),
                },
                RawDenoTask::Detail {
                    command,
                    description,
                    dependencies,
                } => DenoTaskDefinition {
                    command,
                    description,
                    dependencies,
                },
            };
            (name, normalized)
        })
        .collect();

    Ok(Some(ParsedDenoConfig {
        tasks,
        has_workspace: config.workspace.is_some(),
    }))
}

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

    #[test]
    fn wildcard_matches_expected_patterns() {
        assert!(wildcard_matches("build-*", "build-a"));
        assert!(wildcard_matches("build-*", "build-a-dev"));
        assert!(wildcard_matches("*-dev", "build-dev"));
        assert!(wildcard_matches("build-*-dev", "build-a-dev"));
        assert!(wildcard_matches("build-?", "build-a"));
        assert!(!wildcard_matches("build-*", "test-a"));
        assert!(!wildcard_matches("build-*-dev", "build-dev"));
        assert!(!wildcard_matches("build-?", "build-ab"));
    }

    #[test]
    fn wildcard_root_tasks_all_receive_forwarded_args() {
        let mut tasks = IndexMap::new();
        tasks.insert("prepare".to_string(), task("echo prepare", &[]));
        tasks.insert("build-a".to_string(), task("echo a", &["prepare"]));
        tasks.insert("build-b".to_string(), task("echo b", &[]));

        let stages = build_deno_task_stages(
            &tasks,
            &[String::from("build-*")],
            &[String::from("--flag")],
        )
        .unwrap();
        let steps = stages
            .into_iter()
            .flat_map(|stage| stage.steps)
            .map(|step| (step.task_name, step.forward_args))
            .collect::<Vec<_>>();

        assert_eq!(steps.len(), 3);
        assert!(steps.contains(&("prepare".to_string(), false)));
        assert!(steps.contains(&("build-a".to_string(), true)));
        assert!(steps.contains(&("build-b".to_string(), true)));
    }

    fn task(command: &str, dependencies: &[&str]) -> DenoTaskDefinition {
        DenoTaskDefinition {
            command: Some(command.to_string()),
            description: None,
            dependencies: dependencies
                .iter()
                .map(|dependency| (*dependency).to_string())
                .collect(),
        }
    }

    #[test]
    fn reads_jsonc_tasks_and_preserves_order() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(
            dir.path().join("deno.jsonc"),
            r#"{
              // comment
              "tasks": {
                "b": "echo b",
                "a": {
                  "command": "echo a",
                  "description": "task a",
                  "dependencies": ["b"]
                }
              }
            }"#,
        )
        .unwrap();

        let config = read_deno_config(dir.path()).unwrap().unwrap();
        assert_eq!(
            config.tasks.keys().cloned().collect::<Vec<_>>(),
            vec!["b", "a"]
        );
        assert_eq!(config.tasks["a"].description.as_deref(), Some("task a"));
    }

    #[test]
    fn selects_nearest_project_with_deno_config() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("root");
        let nested = root.join("app").join("src");
        fs::create_dir_all(&nested).unwrap();
        fs::write(root.join("deno.json"), r#"{"tasks":{"dev":"echo ok"}}"#).unwrap();

        let project = find_nearest_deno_project(&nested).unwrap().unwrap();
        assert_eq!(project.root, root);
    }
}
