const std = @import("std");

/// Pattern type for ignore rules
const Pattern = struct {
    pattern: []const u8,
    is_negation: bool, // true if pattern starts with !
    is_dir_only: bool, // true if pattern ends with /
    anchored: bool, // true if pattern contains / (not at start)
};

/// GitIgnore matcher for filtering files during directory traversal
pub const GitIgnore = struct {
    patterns: std.ArrayList(Pattern),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) GitIgnore {
        return .{
            .patterns = std.ArrayList(Pattern).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *GitIgnore) void {
        for (self.patterns.items) |p| {
            self.allocator.free(p.pattern);
        }
        self.patterns.deinit();
    }

    /// Load .gitignore or .mlignore from directory
    pub fn loadFromDir(self: *GitIgnore, dir_path: []const u8, filename: []const u8) !void {
        const path = try std.fs.path.join(self.allocator, &[_][]const u8{ dir_path, filename });
        defer self.allocator.free(path);

        const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
            error.FileNotFound => return, // No ignore file is fine
            else => return err,
        };
        defer file.close();

        const content = try file.readToEndAlloc(self.allocator, 1024 * 1024); // Max 1MB
        defer self.allocator.free(content);

        try self.parse(content);
    }

    /// Parse ignore patterns from content
    pub fn parse(self: *GitIgnore, content: []const u8) !void {
        var lines = std.mem.split(u8, content, "\n");
        while (lines.next()) |line| {
            const trimmed = std.mem.trim(u8, line, " \t\r");

            // Skip empty lines and comments
            if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) continue;

            try self.addPattern(trimmed);
        }
    }

    /// Add a single pattern
    fn addPattern(self: *GitIgnore, pattern: []const u8) !void {
        var p = pattern;
        var is_negation = false;
        var is_dir_only = false;

        // Check for negation
        if (std.mem.startsWith(u8, p, "!")) {
            is_negation = true;
            p = p[1..];
        }

        // Check for directory-only marker
        if (std.mem.endsWith(u8, p, "/")) {
            is_dir_only = true;
            p = p[0 .. p.len - 1];
        }

        // Remove leading slash (anchored patterns)
        const anchored = std.mem.indexOf(u8, p, "/") != null;
        if (std.mem.startsWith(u8, p, "/")) {
            p = p[1..];
        }

        // Store normalized pattern
        const pattern_copy = try self.allocator.dupe(u8, p);
        try self.patterns.append(.{
            .pattern = pattern_copy,
            .is_negation = is_negation,
            .is_dir_only = is_dir_only,
            .anchored = anchored,
        });
    }

    /// Check if a path should be ignored
    pub fn isIgnored(self: *GitIgnore, path: []const u8, is_dir: bool) bool {
        var ignored = false;

        for (self.patterns.items) |pattern| {
            if (self.matches(pattern, path, is_dir)) {
                ignored = !pattern.is_negation;
            }
        }

        return ignored;
    }

    /// Check if a single pattern matches
    fn matches(self: *GitIgnore, pattern: Pattern, path: []const u8, is_dir: bool) bool {
        _ = self;

        // Directory-only patterns only match directories
        if (pattern.is_dir_only and !is_dir) return false;

        // Convert gitignore pattern to glob
        if (patternMatch(pattern.pattern, path)) {
            return true;
        }

        // Also check basename match for non-anchored patterns
        if (!pattern.anchored) {
            if (std.mem.lastIndexOf(u8, path, "/")) |idx| {
                const basename = path[idx + 1 ..];
                if (patternMatch(pattern.pattern, basename)) {
                    return true;
                }
            }
        }

        return false;
    }

    /// Simple glob pattern matching
    fn patternMatch(pattern: []const u8, path: []const u8) bool {
        var p_idx: usize = 0;
        var s_idx: usize = 0;

        while (p_idx < pattern.len) {
            const p_char = pattern[p_idx];

            if (p_char == '*') {
                // Handle ** (matches any number of directories)
                if (p_idx + 1 < pattern.len and pattern[p_idx + 1] == '*') {
                    // ** matches everything
                    return true;
                }

                // Single * matches anything until next / or end
                p_idx += 1;
                if (p_idx >= pattern.len) {
                    // * at end - match rest of path
                    return true;
                }

                const next_char = pattern[p_idx];
                while (s_idx < path.len and path[s_idx] != next_char) {
                    s_idx += 1;
                }
            } else if (p_char == '?') {
                // ? matches single character
                if (s_idx >= path.len) return false;
                p_idx += 1;
                s_idx += 1;
            } else {
                // Literal character match
                if (s_idx >= path.len or path[s_idx] != p_char) return false;
                p_idx += 1;
                s_idx += 1;
            }
        }

        return s_idx == path.len;
    }
};

/// Default patterns always ignored (like git does)
pub const DEFAULT_IGNORES = [_][]const u8{
    ".git",
    ".ml",
    "__pycache__",
    "*.pyc",
    "*.pyo",
    ".DS_Store",
    "node_modules",
    ".venv",
    "venv",
    ".env",
    ".idea",
    ".vscode",
    "*.log",
    "*.tmp",
    "*.swp",
    "*.swo",
    "*~",
};

/// Check if path matches default ignores
pub fn matchesDefaultIgnore(path: []const u8) bool {
    // Check exact matches
    for (DEFAULT_IGNORES) |pattern| {
        if (std.mem.eql(u8, path, pattern)) return true;
    }

    // Check suffix matches for patterns like *.pyc
    if (std.mem.lastIndexOf(u8, path, "/")) |idx| {
        const basename = path[idx + 1 ..];
        for (DEFAULT_IGNORES) |pattern| {
            if (std.mem.startsWith(u8, pattern, "*.")) {
                const ext = pattern[1..]; // Get extension including dot
                if (std.mem.endsWith(u8, basename, ext)) return true;
            }
        }
    }

    return false;
}

test "GitIgnore basic patterns" {
    const allocator = std.testing.allocator;

    var gi = GitIgnore.init(allocator);
    defer gi.deinit();

    try gi.parse("node_modules\n__pycache__\n*.pyc\n");

    try std.testing.expect(gi.isIgnored("node_modules", true));
    try std.testing.expect(gi.isIgnored("__pycache__", true));
    try std.testing.expect(gi.isIgnored("test.pyc", false));
    try std.testing.expect(!gi.isIgnored("main.py", false));
}

test "GitIgnore negation" {
    const allocator = std.testing.allocator;

    var gi = GitIgnore.init(allocator);
    defer gi.deinit();

    try gi.parse("*.log\n!important.log\n");

    try std.testing.expect(gi.isIgnored("debug.log", false));
    try std.testing.expect(!gi.isIgnored("important.log", false));
}

test "GitIgnore directory-only" {
    const allocator = std.testing.allocator;

    var gi = GitIgnore.init(allocator);
    defer gi.deinit();

    try gi.parse("build/\n");

    try std.testing.expect(gi.isIgnored("build", true));
    try std.testing.expect(!gi.isIgnored("build", false));
}

test "matchesDefaultIgnore" {
    try std.testing.expect(matchesDefaultIgnore(".git"));
    try std.testing.expect(matchesDefaultIgnore("__pycache__"));
    try std.testing.expect(matchesDefaultIgnore("node_modules"));
    try std.testing.expect(matchesDefaultIgnore("test.pyc"));
    try std.testing.expect(!matchesDefaultIgnore("main.py"));
}
