use std::fs;
use std::path::Path;

use crate::error::{RebeccaError, Result};

use super::ProjectArtifactDefinition;

const PROJECT_ARTIFACT_DEFINITIONS: &[ProjectArtifactDefinition] = &[
    ProjectArtifactDefinition {
        directory_name: "node_modules",
        rule_id: "windows.project-artifact-node-modules",
        restore_hint: "Dependencies can be restored with the project's package manager.",
    },
    ProjectArtifactDefinition {
        directory_name: "target",
        rule_id: "windows.project-artifact-target",
        restore_hint: "Build outputs can be regenerated by the project build tool.",
    },
    ProjectArtifactDefinition {
        directory_name: "build",
        rule_id: "windows.project-artifact-build",
        restore_hint: "Build outputs can be regenerated by the project build tool.",
    },
    ProjectArtifactDefinition {
        directory_name: "dist",
        rule_id: "windows.project-artifact-dist",
        restore_hint: "Distribution outputs can be regenerated by the project build tool.",
    },
    ProjectArtifactDefinition {
        directory_name: "venv",
        rule_id: "windows.project-artifact-python-venv",
        restore_hint: "Python virtual environments can be recreated from project dependencies.",
    },
    ProjectArtifactDefinition {
        directory_name: ".venv",
        rule_id: "windows.project-artifact-python-venv",
        restore_hint: "Python virtual environments can be recreated from project dependencies.",
    },
    ProjectArtifactDefinition {
        directory_name: ".pytest_cache",
        rule_id: "windows.project-artifact-pytest-cache",
        restore_hint: "Pytest cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".mypy_cache",
        rule_id: "windows.project-artifact-mypy-cache",
        restore_hint: "Mypy cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".tox",
        rule_id: "windows.project-artifact-tox-cache",
        restore_hint: "Tox environments can be recreated from project dependencies.",
    },
    ProjectArtifactDefinition {
        directory_name: ".nox",
        rule_id: "windows.project-artifact-nox-cache",
        restore_hint: "Nox environments can be recreated from project dependencies.",
    },
    ProjectArtifactDefinition {
        directory_name: ".ruff_cache",
        rule_id: "windows.project-artifact-ruff-cache",
        restore_hint: "Ruff cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: "__pycache__",
        rule_id: "windows.project-artifact-python-bytecode",
        restore_hint: "Python bytecode cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".gradle",
        rule_id: "windows.project-artifact-gradle-cache",
        restore_hint: "Gradle project cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".next",
        rule_id: "windows.project-artifact-next-cache",
        restore_hint: "Next.js build output can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".nuxt",
        rule_id: "windows.project-artifact-nuxt-cache",
        restore_hint: "Nuxt build output can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".output",
        rule_id: "windows.project-artifact-nuxt-output",
        restore_hint: "Nuxt output can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".turbo",
        rule_id: "windows.project-artifact-turbo-cache",
        restore_hint: "Turborepo cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".parcel-cache",
        rule_id: "windows.project-artifact-parcel-cache",
        restore_hint: "Parcel cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".dart_tool",
        rule_id: "windows.project-artifact-dart-tool",
        restore_hint: "Dart and Flutter tool data can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".zig-cache",
        rule_id: "windows.project-artifact-zig-cache",
        restore_hint: "Zig cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: "zig-out",
        rule_id: "windows.project-artifact-zig-out",
        restore_hint: "Zig output can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".angular",
        rule_id: "windows.project-artifact-angular-cache",
        restore_hint: "Angular cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".svelte-kit",
        rule_id: "windows.project-artifact-svelte-kit",
        restore_hint: "SvelteKit build data can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: ".astro",
        rule_id: "windows.project-artifact-astro-cache",
        restore_hint: "Astro build data can be regenerated.",
    },
    ProjectArtifactDefinition {
        directory_name: "coverage",
        rule_id: "windows.project-artifact-coverage",
        restore_hint: "Coverage reports can be regenerated by test tooling.",
    },
    ProjectArtifactDefinition {
        directory_name: "Pods",
        rule_id: "windows.project-artifact-cocoapods",
        restore_hint: "CocoaPods dependencies can be restored with pod install.",
    },
    ProjectArtifactDefinition {
        directory_name: ".cxx",
        rule_id: "windows.project-artifact-cxx-cache",
        restore_hint: "C/C++ build cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: "obj",
        rule_id: "windows.project-artifact-dotnet-obj",
        restore_hint: ".NET object output can be regenerated by the build tool.",
    },
    ProjectArtifactDefinition {
        directory_name: ".expo",
        rule_id: "windows.project-artifact-expo-cache",
        restore_hint: "Expo cache data is rebuildable.",
    },
    ProjectArtifactDefinition {
        directory_name: ".build",
        rule_id: "windows.project-artifact-swift-build",
        restore_hint: "Swift build output can be regenerated.",
    },
];

const CACHEDIR_TAG_DEFINITION: ProjectArtifactDefinition = ProjectArtifactDefinition {
    directory_name: "CACHEDIR.TAG",
    rule_id: "windows.project-artifact-cachedir-tag",
    restore_hint: "CACHEDIR.TAG marks this directory as rebuildable cache data.",
};

const DOTNET_BIN_DEFINITION: ProjectArtifactDefinition = ProjectArtifactDefinition {
    directory_name: "bin",
    rule_id: "windows.project-artifact-dotnet-bin",
    restore_hint: ".NET bin outputs can be regenerated by the project build tool.",
};

const COMPOSER_VENDOR_DEFINITION: ProjectArtifactDefinition = ProjectArtifactDefinition {
    directory_name: "vendor",
    rule_id: "windows.project-artifact-composer-vendor",
    restore_hint: "Composer vendor dependencies can be restored with composer install.",
};

const PRUNED_SCAN_DIRS: &[&str] = &[
    ".git",
    ".hg",
    ".svn",
    ".trash",
    "$recycle.bin",
    "library",
    "applications",
];

pub fn project_artifact_definitions() -> &'static [ProjectArtifactDefinition] {
    PROJECT_ARTIFACT_DEFINITIONS
}

pub fn all_project_artifact_definitions() -> impl Iterator<Item = ProjectArtifactDefinition> {
    PROJECT_ARTIFACT_DEFINITIONS.iter().copied().chain([
        CACHEDIR_TAG_DEFINITION,
        DOTNET_BIN_DEFINITION,
        COMPOSER_VENDOR_DEFINITION,
    ])
}

pub fn project_artifact_definition_for_dir_name(name: &str) -> Option<ProjectArtifactDefinition> {
    PROJECT_ARTIFACT_DEFINITIONS
        .iter()
        .copied()
        .find(|definition| definition.directory_name.eq_ignore_ascii_case(name))
}

pub fn validate_project_artifact_selectors(selectors: &[String]) -> Result<()> {
    for selector in selectors {
        if selector.trim().is_empty() {
            return Err(RebeccaError::InvalidProjectArtifactSelector(
                "selector cannot be empty".to_string(),
            ));
        }

        let known = all_project_artifact_definitions()
            .any(|definition| project_artifact_matches_selector(definition, selector));
        if !known {
            return Err(RebeccaError::InvalidProjectArtifactSelector(
                selector.clone(),
            ));
        }
    }

    Ok(())
}

pub fn project_artifact_matches_selectors(
    definition: ProjectArtifactDefinition,
    selectors: &[String],
) -> bool {
    selectors.is_empty()
        || selectors
            .iter()
            .any(|selector| project_artifact_matches_selector(definition, selector))
}

pub(super) fn definition_for_directory(
    dir: &Path,
    name: &str,
) -> Option<ProjectArtifactDefinition> {
    context_definition_for_directory(dir, name)
        .or_else(|| project_artifact_definition_for_dir_name(name))
}

pub(super) fn cachedir_tag_definition() -> ProjectArtifactDefinition {
    CACHEDIR_TAG_DEFINITION
}

pub(super) fn should_prune_scan_dir(name: &str) -> bool {
    PRUNED_SCAN_DIRS
        .iter()
        .any(|pruned| pruned.eq_ignore_ascii_case(name))
}

fn project_artifact_matches_selector(
    definition: ProjectArtifactDefinition,
    selector: &str,
) -> bool {
    let selector = selector.trim();
    if selector.eq_ignore_ascii_case(definition.rule_id)
        || selector.eq_ignore_ascii_case(definition.directory_name)
    {
        return true;
    }

    let rule_suffix = definition
        .rule_id
        .strip_prefix("windows.project-artifact-")
        .unwrap_or(definition.rule_id);
    if selector.eq_ignore_ascii_case(rule_suffix) {
        return true;
    }

    selector_alias(selector) == selector_alias(definition.directory_name)
}

fn selector_alias(value: &str) -> String {
    value.trim().to_ascii_lowercase().replace(['_', '.'], "-")
}

fn context_definition_for_directory(dir: &Path, name: &str) -> Option<ProjectArtifactDefinition> {
    if name.eq_ignore_ascii_case(DOTNET_BIN_DEFINITION.directory_name) && is_dotnet_bin_dir(dir) {
        return Some(DOTNET_BIN_DEFINITION);
    }

    if name.eq_ignore_ascii_case(COMPOSER_VENDOR_DEFINITION.directory_name)
        && is_composer_vendor_dir(dir)
    {
        return Some(COMPOSER_VENDOR_DEFINITION);
    }

    None
}

fn is_dotnet_bin_dir(path: &Path) -> bool {
    let Some(parent) = path.parent() else {
        return false;
    };

    has_dotnet_project_file(parent)
        && (has_child_dir(path, "Debug") || has_child_dir(path, "Release"))
}

fn has_dotnet_project_file(dir: &Path) -> bool {
    let Ok(entries) = fs::read_dir(dir) else {
        return false;
    };

    entries.flatten().any(|entry| {
        let path = entry.path();
        let Ok(metadata) = fs::symlink_metadata(&path) else {
            return false;
        };
        if !metadata.is_file() || crate::safety::is_reparse_like(&metadata) {
            return false;
        }

        path.extension()
            .and_then(|extension| extension.to_str())
            .is_some_and(|extension| {
                ["csproj", "fsproj", "vbproj"]
                    .iter()
                    .any(|expected| extension.eq_ignore_ascii_case(expected))
            })
    })
}

fn is_composer_vendor_dir(path: &Path) -> bool {
    let Some(parent) = path.parent() else {
        return false;
    };

    has_regular_file(&parent.join("composer.json"))
}

fn has_child_dir(parent: &Path, name: &str) -> bool {
    let path = parent.join(name);
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return false;
    };

    metadata.is_dir() && !crate::safety::is_reparse_like(&metadata)
}

fn has_regular_file(path: &Path) -> bool {
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return false;
    };

    metadata.is_file() && !crate::safety::is_reparse_like(&metadata)
}
