use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

fn shell_single_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "'\\''"))
}

fn command_in_dir(dir: Option<&Path>, command: &str) -> String {
    match dir {
        Some(path) => format!(
            "cd {} && {}",
            shell_single_quote(&path.to_string_lossy()),
            command
        ),
        None => command.to_string(),
    }
}

fn package_json_scripts(dir: &Path) -> HashMap<String, String> {
    json_scripts(&dir.join("package.json"))
}

fn json_scripts(path: &Path) -> HashMap<String, String> {
    let Ok(text) = std::fs::read_to_string(path) else {
        return HashMap::new();
    };
    let Ok(value) = serde_json::from_str::<Value>(&text) else {
        return HashMap::new();
    };
    value
        .get("scripts")
        .and_then(Value::as_object)
        .map(|scripts| {
            scripts
                .iter()
                .filter_map(|(name, script)| {
                    script
                        .as_str()
                        .map(|script| (name.to_string(), script.to_string()))
                })
                .collect()
        })
        .unwrap_or_default()
}

fn composer_scripts(dir: &Path) -> HashMap<String, String> {
    json_scripts(&dir.join("composer.json"))
}

fn npm_runner(dir: &Path) -> &'static str {
    if dir.join("pnpm-lock.yaml").exists() {
        "pnpm"
    } else if dir.join("yarn.lock").exists() {
        "yarn"
    } else if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() {
        "bun"
    } else {
        "npm"
    }
}

fn package_script_command(runner: &str, script: &str) -> String {
    match (runner, script) {
        ("npm", "test") => "npm test".to_string(),
        ("yarn", "test") => "yarn test".to_string(),
        ("pnpm", "test") => "pnpm test".to_string(),
        ("bun", "test") => "bun test".to_string(),
        ("npm", other) => format!("npm run {other}"),
        ("yarn", other) => format!("yarn {other}"),
        ("pnpm", other) => format!("pnpm {other}"),
        ("bun", other) => format!("bun run {other}"),
        (_, other) => format!("npm run {other}"),
    }
}

fn makefile_has_target(dir: &Path, target: &str) -> bool {
    let path = dir.join("Makefile");
    let Ok(text) = std::fs::read_to_string(&path) else {
        return false;
    };
    text.lines().any(|line| {
        let line = line.trim_start();
        line.starts_with(&format!("{target}:")) || line.starts_with(&format!(".PHONY: {target}"))
    })
}

fn prompt_mentions(prompt: &str, needles: &[&str]) -> bool {
    let lower = prompt.to_ascii_lowercase();
    needles.iter().any(|needle| lower.contains(needle))
}

fn prompt_file_candidate(prompt: &str) -> Option<String> {
    for raw in prompt.split_whitespace() {
        let token = raw.trim_matches(|c: char| {
            matches!(
                c,
                '"' | '\'' | '`' | ',' | ';' | ':' | ')' | '(' | '[' | ']' | '{' | '}'
            )
        });
        if token.is_empty() || token.starts_with('-') || token.contains("://") {
            continue;
        }
        let looks_path = token.contains('/')
            || token.ends_with(".md")
            || token.ends_with(".txt")
            || token.ends_with(".json")
            || token.ends_with(".yaml")
            || token.ends_with(".yml")
            || token.ends_with(".toml")
            || token.ends_with(".html")
            || token.ends_with(".css")
            || token.ends_with(".js")
            || token.ends_with(".ts")
            || token.ends_with(".tsx")
            || token.ends_with(".rs")
            || token.ends_with(".py")
            || token.ends_with(".go")
            || token.ends_with(".swift")
            || token.ends_with(".kt")
            || token.ends_with(".kts")
            || token.ends_with(".java")
            || token.ends_with(".c")
            || token.ends_with(".h")
            || token.ends_with(".cc")
            || token.ends_with(".hh")
            || token.ends_with(".cpp")
            || token.ends_with(".hpp")
            || token.ends_with(".cxx")
            || token.ends_with(".hxx")
            || token.ends_with(".cs")
            || token.ends_with(".fs")
            || token.ends_with(".vb")
            || token.ends_with(".php")
            || token.ends_with(".rb")
            || token.ends_with(".ex")
            || token.ends_with(".exs");
        if looks_path && !token.starts_with('/') && !token.contains("..") {
            return Some(token.to_string());
        }
    }
    None
}

struct ProjectCheck {
    command: String,
    confidence: &'static str,
    rationale: String,
    signals: Vec<String>,
    warnings: Vec<String>,
}

struct LocatedProjectCheck {
    dir: PathBuf,
    file_for_check: String,
    project: ProjectCheck,
    project_dir_signal: Option<String>,
}

fn project_check_for_prompt(dir: &Path, prompt: &str) -> Option<ProjectCheck> {
    if dir.join("Cargo.toml").exists() {
        return Some(ProjectCheck {
            command: "cargo test -q".to_string(),
            confidence: "high",
            rationale:
                "Rust project detected; cargo test is the broadest deterministic completion check."
                    .to_string(),
            signals: vec!["Cargo.toml".to_string()],
            warnings: Vec::new(),
        });
    }

    let mut skipped_signals: Vec<String> = Vec::new();
    let mut skipped_warnings: Vec<String> = Vec::new();

    if dir.join("package.json").exists() {
        let scripts = package_json_scripts(dir);
        let runner = npm_runner(dir);
        let preference: &[&str] = if prompt_mentions(prompt, &["type", "typescript", "tsc"]) {
            &["typecheck", "check", "test", "build"]
        } else if prompt_mentions(prompt, &["check", "verify", "ci"]) {
            &["check", "test", "typecheck", "build", "lint"]
        } else if prompt_mentions(prompt, &["build", "compile", "bundle"]) {
            &["build", "test", "typecheck", "check"]
        } else if prompt_mentions(prompt, &["lint", "format"]) {
            &["lint", "test", "typecheck", "build"]
        } else {
            &["test", "typecheck", "check", "build", "lint"]
        };
        for script in preference {
            if scripts.contains_key(*script) {
                return Some(ProjectCheck {
                    command: package_script_command(runner, script),
                    confidence: "high",
                    rationale: format!(
                        "Node project detected; selected the `{script}` script from package.json."
                    ),
                    signals: vec![
                        "package.json".to_string(),
                        format!("package_script:{script}"),
                    ],
                    warnings: Vec::new(),
                });
            }
        }
        skipped_signals.push("package.json".to_string());
        skipped_warnings
            .push("package.json has no test/typecheck/check/build/lint script".to_string());
    }

    if dir.join("pyproject.toml").exists() || dir.join("setup.py").exists() {
        let has_tests = dir.join("tests").is_dir();
        let mut signals = skipped_signals;
        signals.push("python_project".to_string());
        return Some(ProjectCheck {
            command: if has_tests {
                "python -m pytest".to_string()
            } else {
                "python -m compileall .".to_string()
            },
            confidence: if has_tests { "high" } else { "medium" },
            rationale: if has_tests {
                "Python project with tests/ detected; pytest is a deterministic completion check."
                    .to_string()
            } else {
                "Python project detected but no tests/ directory was found; compileall is a syntax-level fallback."
                    .to_string()
            },
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("go.mod").exists() {
        let mut signals = skipped_signals;
        signals.push("go.mod".to_string());
        return Some(ProjectCheck {
            command: "go test ./...".to_string(),
            confidence: "high",
            rationale:
                "Go module detected; go test ./... is the standard deterministic completion check."
                    .to_string(),
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("Package.swift").exists() {
        let mut signals = skipped_signals;
        signals.push("Package.swift".to_string());
        return Some(ProjectCheck {
            command: "swift test".to_string(),
            confidence: "high",
            rationale:
                "Swift package detected; swift test is the standard deterministic completion check."
                    .to_string(),
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("CMakeLists.txt").exists() {
        let mut signals = skipped_signals;
        signals.push("CMakeLists.txt".to_string());
        let use_ctest = prompt_mentions(prompt, &["test", "ctest", "unit", "ci", "verify"]);
        return Some(ProjectCheck {
            command: if use_ctest {
                "cmake -S . -B build && cmake --build build && ctest --test-dir build --output-on-failure"
                    .to_string()
            } else {
                "cmake -S . -B build && cmake --build build".to_string()
            },
            confidence: "high",
            rationale: if use_ctest {
                "CMake project detected; configure, build, and ctest provide a deterministic completion check."
                    .to_string()
            } else {
                "CMake project detected; configure and build provide a deterministic completion check."
                    .to_string()
            },
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("build.gradle.kts").exists() || dir.join("build.gradle").exists() {
        let mut signals = skipped_signals;
        let build_file = if dir.join("build.gradle.kts").exists() {
            "build.gradle.kts"
        } else {
            "build.gradle"
        };
        signals.push(build_file.to_string());
        let has_wrapper = dir.join("gradlew").exists();
        if has_wrapper {
            signals.push("gradlew".to_string());
        }
        return Some(ProjectCheck {
            command: if has_wrapper {
                "./gradlew test".to_string()
            } else {
                "gradle test".to_string()
            },
            confidence: "high",
            rationale: if has_wrapper {
                "Gradle project detected with wrapper; ./gradlew test is the deterministic completion check."
                    .to_string()
            } else {
                "Gradle project detected; gradle test is the standard deterministic completion check."
                    .to_string()
            },
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("pom.xml").exists() {
        let mut signals = skipped_signals;
        signals.push("pom.xml".to_string());
        let has_wrapper = dir.join("mvnw").exists();
        if has_wrapper {
            signals.push("mvnw".to_string());
        }
        return Some(ProjectCheck {
            command: if has_wrapper {
                "./mvnw test".to_string()
            } else {
                "mvn test".to_string()
            },
            confidence: "high",
            rationale: if has_wrapper {
                "Maven project detected with wrapper; ./mvnw test is the deterministic completion check."
                    .to_string()
            } else {
                "Maven project detected; mvn test is the standard deterministic completion check."
                    .to_string()
            },
            signals,
            warnings: skipped_warnings,
        });
    }

    if let Some(dotnet_file) = dotnet_project_signal(dir) {
        let mut signals = skipped_signals;
        signals.push(dotnet_file);
        return Some(ProjectCheck {
            command: "dotnet test".to_string(),
            confidence: "high",
            rationale:
                ".NET project detected; dotnet test is the standard deterministic completion check."
                    .to_string(),
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("composer.json").exists() {
        let mut signals = skipped_signals;
        signals.push("composer.json".to_string());
        let has_phpunit =
            dir.join("vendor/bin/phpunit").exists() || dir.join("phpunit.xml").exists();
        if has_phpunit {
            signals.push("phpunit".to_string());
        }
        let scripts = composer_scripts(dir);
        let has_test_script = scripts.contains_key("test");
        if has_test_script {
            signals.push("composer_script:test".to_string());
        }
        return Some(ProjectCheck {
            command: if has_phpunit {
                "vendor/bin/phpunit".to_string()
            } else if has_test_script {
                "composer test".to_string()
            } else {
                "find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l"
                    .to_string()
            },
            confidence: if has_phpunit || has_test_script {
                "high"
            } else {
                "medium"
            },
            rationale: if has_phpunit {
                "PHP Composer project with phpunit detected; vendor/bin/phpunit is the deterministic completion check."
                    .to_string()
            } else if has_test_script {
                "PHP Composer project with a test script detected; composer test is the deterministic completion check."
                    .to_string()
            } else {
                "PHP Composer project detected without a test script; PHP linting is a deterministic syntax-level fallback."
                    .to_string()
            },
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("Gemfile").exists() {
        let mut signals = skipped_signals;
        signals.push("Gemfile".to_string());
        let ruby = ruby_project_check(dir);
        signals.extend(ruby.signals);
        return Some(ProjectCheck {
            command: ruby.command,
            confidence: ruby.confidence,
            rationale: ruby.rationale,
            signals,
            warnings: skipped_warnings,
        });
    }

    if dir.join("mix.exs").exists() {
        let mut signals = skipped_signals;
        signals.push("mix.exs".to_string());
        return Some(ProjectCheck {
            command: "mix test".to_string(),
            confidence: "high",
            rationale:
                "Elixir Mix project detected; mix test is the standard deterministic completion check."
                    .to_string(),
            signals,
            warnings: skipped_warnings,
        });
    }

    if makefile_has_target(dir, "test") {
        let mut signals = skipped_signals;
        signals.push("Makefile:test".to_string());
        return Some(ProjectCheck {
            command: "make test".to_string(),
            confidence: "medium",
            rationale: "Makefile test target detected.".to_string(),
            signals,
            warnings: skipped_warnings,
        });
    }

    if skipped_warnings.is_empty() {
        None
    } else {
        Some(ProjectCheck {
            command: String::new(),
            confidence: "none",
            rationale: String::new(),
            signals: skipped_signals,
            warnings: skipped_warnings,
        })
    }
}

fn path_for_shell(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn project_check_for_path(
    dir: &Path,
    file: &str,
    prompt: &str,
    target_may_be_directory: bool,
) -> Option<LocatedProjectCheck> {
    let file_path = dir.join(file);
    let mut candidate = if target_may_be_directory && file_path.is_dir() {
        file_path.clone()
    } else {
        file_path.parent()?.to_path_buf()
    };
    loop {
        if let Some(project) = project_check_for_prompt(&candidate, prompt) {
            let file_for_check = file_path.strip_prefix(&candidate).ok()?;
            let project_dir_signal = candidate
                .strip_prefix(dir)
                .ok()
                .filter(|relative| !relative.as_os_str().is_empty())
                .map(|relative| format!("project_dir:{}", path_for_shell(relative)));
            return Some(LocatedProjectCheck {
                dir: candidate,
                file_for_check: path_for_shell(file_for_check),
                project,
                project_dir_signal,
            });
        }
        if candidate == dir || !candidate.pop() {
            break;
        }
    }
    None
}

fn dotnet_project_signal(dir: &Path) -> Option<String> {
    let entries = std::fs::read_dir(dir).ok()?;
    let mut project_file: Option<String> = None;
    for entry in entries.flatten() {
        let path = entry.path();
        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
            continue;
        };
        if file_name.ends_with(".sln") {
            return Some(file_name.to_string());
        }
        if project_file.is_none()
            && (file_name.ends_with(".csproj")
                || file_name.ends_with(".fsproj")
                || file_name.ends_with(".vbproj"))
        {
            project_file = Some(file_name.to_string());
        }
    }
    project_file
}

struct RubyCheck {
    command: String,
    confidence: &'static str,
    rationale: String,
    signals: Vec<String>,
}

fn ruby_project_check(dir: &Path) -> RubyCheck {
    if dir.join("Rakefile").exists() {
        return RubyCheck {
            command: "bundle exec rake test".to_string(),
            confidence: "high",
            rationale:
                "Ruby Bundler project with Rakefile detected; bundle exec rake test is the deterministic completion check."
                    .to_string(),
            signals: vec!["Rakefile".to_string()],
        };
    }
    if dir.join("spec").is_dir() {
        return RubyCheck {
            command: "bundle exec rspec".to_string(),
            confidence: "medium",
            rationale:
                "Ruby Bundler project with spec/ detected; bundle exec rspec is the deterministic completion check."
                    .to_string(),
            signals: vec!["spec".to_string()],
        };
    }
    RubyCheck {
        command: "find . -name '*.rb' -not -path './vendor/*' -print0 | xargs -0 -n1 ruby -c"
            .to_string(),
        confidence: "medium",
        rationale:
            "Ruby Bundler project detected without a test runner signal; Ruby syntax checking is a deterministic fallback."
                .to_string(),
        signals: Vec::new(),
    }
}

/// Synthesize a deterministic completion check from concrete project signals.
///
/// This is intentionally conservative: it returns `suggested: false` when it
/// cannot infer a real command/path predicate, rather than converting vague text
/// into a model-judged condition.
pub fn synthesize_goal_check(prompt: &str, cwd: Option<&Path>) -> Value {
    let dir = cwd.filter(|p| p.is_dir());
    let mut signals: Vec<String> = Vec::new();
    let mut warnings: Vec<String> = Vec::new();

    if prompt_mentions(
        prompt,
        &["create", "write", "generate", "save", "output", "produce"],
    ) {
        if let Some(file) = prompt_file_candidate(prompt) {
            signals.push(format!("prompt_mentions_path:{file}"));
            if let Some(dir) = dir {
                if let Some(located) = project_check_for_path(dir, &file, prompt, false) {
                    let LocatedProjectCheck {
                        dir: project_dir,
                        file_for_check,
                        project,
                        project_dir_signal,
                    } = located;
                    signals.extend(project.signals);
                    if let Some(project_dir_signal) = project_dir_signal {
                        signals.push(project_dir_signal);
                    }
                    warnings.extend(project.warnings);
                    if !project.command.is_empty() {
                        let file_check = format!("test -f {}", shell_single_quote(&file_for_check));
                        let command = format!("{file_check} && {}", project.command);
                        return serde_json::json!({
                            "suggested": true,
                            "goal": {
                                "check": command_in_dir(Some(&project_dir), &command),
                                "max_iterations": 8,
                            },
                            "confidence": project.confidence,
                            "rationale": format!(
                                "The prompt names `{file}` and a project check is available. Completion requires the file to exist and the project verifier to pass. {}",
                                project.rationale
                            ),
                            "signals": signals,
                            "warnings": warnings,
                        });
                    }
                }
            }
            return serde_json::json!({
                "suggested": true,
                "goal": {
                    "check": command_in_dir(dir, &format!("test -f {}", shell_single_quote(&file))),
                    "max_iterations": 8,
                },
                "confidence": "medium",
                "rationale": format!("The prompt names `{file}`, so file existence is a concrete completion check."),
                "signals": signals,
                "warnings": warnings,
            });
        }
    }

    if let Some(dir) = dir {
        if let Some(file) = prompt_file_candidate(prompt) {
            if let Some(located) = project_check_for_path(dir, &file, prompt, true) {
                let LocatedProjectCheck {
                    dir: project_dir,
                    project,
                    project_dir_signal,
                    ..
                } = located;
                signals.push(format!("prompt_mentions_path:{file}"));
                signals.extend(project.signals);
                if let Some(project_dir_signal) = project_dir_signal {
                    signals.push(project_dir_signal);
                }
                warnings.extend(project.warnings);
                if !project.command.is_empty() {
                    return serde_json::json!({
                        "suggested": true,
                        "goal": {
                            "check": command_in_dir(Some(&project_dir), &project.command),
                            "max_iterations": 8,
                        },
                        "confidence": project.confidence,
                        "rationale": format!(
                            "The prompt names `{file}` inside a recognized project. Completion requires the nearest project verifier to pass. {}",
                            project.rationale
                        ),
                        "signals": signals,
                        "warnings": warnings,
                    });
                }
            }
        }

        if let Some(project) = project_check_for_prompt(dir, prompt) {
            signals.extend(project.signals);
            warnings.extend(project.warnings);
            if !project.command.is_empty() {
                let ProjectCheck {
                    command,
                    confidence,
                    rationale,
                    ..
                } = project;
                return serde_json::json!({
                    "suggested": true,
                    "goal": {
                        "check": command_in_dir(Some(dir), &command),
                        "max_iterations": 8,
                    },
                    "confidence": confidence,
                    "rationale": rationale,
                    "signals": signals,
                    "warnings": warnings,
                });
            }
        }

        if makefile_has_target(dir, "test") {
            signals.push("Makefile:test".to_string());
            return serde_json::json!({
                "suggested": true,
                "goal": {
                    "check": command_in_dir(Some(dir), "make test"),
                    "max_iterations": 8,
                },
                "confidence": "medium",
                "rationale": "Makefile test target detected.",
                "signals": signals,
                "warnings": warnings,
            });
        }
    } else if cwd.is_some() {
        warnings.push("working_dir/cwd was supplied but is not a directory".to_string());
    }

    serde_json::json!({
        "suggested": false,
        "goal": Value::Null,
        "confidence": "none",
        "rationale": "No deterministic project or file-existence check could be inferred. Ask the user for a concrete success condition before starting a goal loop.",
        "signals": signals,
        "warnings": warnings,
    })
}
