const std = @import("std");
const types = @import("types.zig");

// Fast man page discovery by directly scanning filesystem
const ManSearcher = struct {
    allocator: std.mem.Allocator,
    entries: types.ManEntryList,

    const Self = @This();

    // Common man page directories in priority order
    const MAN_DIRS = [_][]const u8{
        "/usr/share/man", // Standard Linux/macOS
        "/usr/local/share/man", // Local installations
        "/opt/homebrew/share/man", // Homebrew on macOS
        "/opt/local/share/man", // MacPorts on macOS
        "/usr/X11R6/man", // X11 on some systems
        "/usr/pkg/man", // pkgsrc on NetBSD
    };

    // File extensions to look for (compressed and uncompressed)
    const EXTENSIONS = [_][]const u8{ "", ".gz", ".bz2", ".xz", ".lz", ".Z" };

    pub fn init(allocator: std.mem.Allocator) Self {
        return Self{
            .allocator = allocator,
            .entries = .{},
        };
    }

    pub fn deinit(self: *Self) void {
        for (self.entries.items) |entry| {
            self.allocator.free(entry.name);
            self.allocator.free(entry.section);
            self.allocator.free(entry.description);
            if (entry.path) |path| self.allocator.free(path);
        }
        self.entries.deinit(self.allocator);
    }

    // Extract man page name and section from filename
    fn parseManFilename(filename: []const u8) ?struct { name: []const u8, section: []const u8 } {
        // Handle compressed extensions first
        var base_name = filename;
        for (EXTENSIONS[1..]) |ext| { // Skip empty extension
            if (std.mem.endsWith(u8, base_name, ext)) {
                base_name = base_name[0 .. base_name.len - ext.len];
                break;
            }
        }

        // Find the last dot to separate name from section
        if (std.mem.lastIndexOf(u8, base_name, ".")) |dot_pos| {
            if (dot_pos > 0 and dot_pos < base_name.len - 1) {
                const name = base_name[0..dot_pos];
                const section = base_name[dot_pos + 1 ..];

                // Basic validation - section should be reasonable
                if (section.len > 0 and section.len <= 4) {
                    return .{ .name = name, .section = section };
                }
            }
        }

        return null;
    }

    // Get description from man file header (first line parsing)
    fn extractDescription(self: Self, file_path: []const u8) []const u8 {
        // Try to read the first few lines to extract description
        const file = std.fs.cwd().openFile(file_path, .{}) catch return "";
        defer file.close();

        var buffer: [1024]u8 = undefined;
        const bytes_read = file.read(&buffer) catch return "";

        if (bytes_read == 0) return "";

        const content = buffer[0..bytes_read];

        // For compressed files, we'd need to decompress first
        // For now, return empty description for compressed files
        if (std.mem.indexOf(u8, file_path, ".gz") != null or
            std.mem.indexOf(u8, file_path, ".bz2") != null or
            std.mem.indexOf(u8, file_path, ".xz") != null)
        {
            return "";
        }

        // Look for NAME section and extract description
        var lines = std.mem.splitScalar(u8, content, '\n');
        var in_name_section = false;

        while (lines.next()) |line| {
            const trimmed = std.mem.trim(u8, line, " \t\r\n");

            if (std.mem.eql(u8, trimmed, ".SH NAME") or
                std.mem.eql(u8, trimmed, ".SH \"NAME\""))
            {
                in_name_section = true;
                continue;
            }

            if (in_name_section) {
                if (std.mem.startsWith(u8, trimmed, ".SH")) {
                    break; // End of NAME section
                }

                // Look for the description line (usually contains \-)
                if (std.mem.indexOf(u8, trimmed, "\\-")) |dash_pos| {
                    const desc_start = dash_pos + 2;
                    if (desc_start < trimmed.len) {
                        const desc = std.mem.trim(u8, trimmed[desc_start..], " \t");
                        return self.allocator.dupe(u8, desc) catch "";
                    }
                }
            }
        }

        return "";
    }

    // Scan a single man directory section (e.g., man1, man2, etc.)
    fn scanManSection(self: *Self, base_dir: []const u8, section: []const u8, show_paths: bool) !void {
        var path_buffer: [512]u8 = undefined;
        const section_path = try std.fmt.bufPrint(path_buffer[0..], "{s}/man{s}", .{ base_dir, section });

        var dir = std.fs.cwd().openDir(section_path, .{ .iterate = true }) catch return;
        defer dir.close();

        var iterator = dir.iterate();
        while (try iterator.next()) |entry| {
            if (entry.kind != .file) continue;

            // Parse the filename to get name and section
            if (parseManFilename(entry.name)) |parsed| {
                // Verify section matches directory
                if (!std.mem.eql(u8, parsed.section, section)) continue;

                const full_path = if (show_paths) blk: {
                    var full_path_buf: [768]u8 = undefined;
                    const full_path_str = try std.fmt.bufPrint(full_path_buf[0..], "{s}/{s}", .{ section_path, entry.name });
                    break :blk try self.allocator.dupe(u8, full_path_str);
                } else null;

                // Extract description (simplified - just use empty for now)
                // In a production implementation, you'd want to parse the man file header
                const description = if (show_paths)
                    self.extractDescription(full_path.?)
                else
                    try self.allocator.dupe(u8, "");

                const man_entry = types.ManEntry{
                    .name = try self.allocator.dupe(u8, parsed.name),
                    .section = try self.allocator.dupe(u8, parsed.section),
                    .description = description,
                    .path = full_path,
                };

                try self.entries.append(self.allocator, man_entry);
            }
        }
    }

    // Main search function - much faster than man -k
    // In the searchManPages function of ManSearcher:
    pub fn searchManPages(
        self: *Self,
        keyword: []const u8,
        target_sections: ?[]const []const u8,
        show_paths: bool,
        verbose: bool,
        debug_writer: *std.Io.Writer,
    ) !void {
        if (verbose) {
            try debug_writer.print("[=] Scanning man page directories directly...\n", .{});
            try debug_writer.flush();
        }

        // Sections to scan - handle multiple sections
        const sections = if (target_sections) |sects|
            sects // Use provided sections array
        else
            &[_][]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "1m", "3c", "3x", "n" };

        for (MAN_DIRS) |man_dir| {
            // Check if directory exists
            var dir = std.fs.cwd().openDir(man_dir, .{}) catch |err| switch (@as(anyerror, err)) {
                error.FileNotFound, error.NotDir, error.AccessDenied => continue,
                else => return err,
            };
            dir.close();

            if (verbose) {
                try debug_writer.print("[=] Scanning {s}...\n", .{man_dir});
                try debug_writer.flush();
            }

            for (sections) |section| {
                self.scanManSection(man_dir, section, show_paths) catch |err| switch (@as(anyerror, err)) {
                    error.FileNotFound, error.NotDir, error.AccessDenied => continue,
                    else => return err,
                };
            }
        }

        // Filter by keyword (case-insensitive search in name)
        var filtered_entries = types.ManEntryList{};
        defer {
            // Move entries to self.entries and clean up filtered_entries
            self.entries.deinit(self.allocator);
            self.entries = filtered_entries;
        }

        for (self.entries.items) |entry| {
            // Simple case-insensitive substring search
            var name_lower: [256]u8 = undefined;
            var keyword_lower: [256]u8 = undefined;

            if (entry.name.len < name_lower.len and keyword.len < keyword_lower.len) {
                _ = std.ascii.lowerString(name_lower[0..entry.name.len], entry.name);
                _ = std.ascii.lowerString(keyword_lower[0..keyword.len], keyword);

                if (std.mem.indexOf(u8, name_lower[0..entry.name.len], keyword_lower[0..keyword.len]) != null) {
                    try filtered_entries.append(self.allocator, entry);
                } else {
                    // Clean up unused entry
                    self.allocator.free(entry.name);
                    self.allocator.free(entry.section);
                    self.allocator.free(entry.description);
                    if (entry.path) |path| self.allocator.free(path);
                }
            }
        }
    }
};

// Updated search function that uses direct filesystem scanning
pub fn searchManPagesFast(
    keyword: []const u8,
    target_section: ?[]const []const u8,
    show_paths: bool,
    verbose: bool,
    allocator: std.mem.Allocator,
    stderr: *std.Io.Writer, // Post-writergate: concrete type
) !types.ManEntryList {
    var searcher = ManSearcher.init(allocator);
    defer searcher.deinit();

    // Create debug writer for verbose output - using stderr for debug messages
    try searcher.searchManPages(keyword, target_section, show_paths, verbose, stderr);

    if (searcher.entries.items.len == 0) {
        if (target_section) |sections| {
            // Join sections for display
            var joined: [32]u8 = undefined;
            var pos: usize = 0;
            for (sections, 0..) |s, i| {
                if (i != 0) {
                    joined[pos] = ',';
                    pos += 1;
                }
                @memcpy(joined[pos .. pos + s.len], s);
                pos += s.len;
            }
            try stderr.print("[X] No matches found for '{s}' in section {s}\n", .{ keyword, joined[0..pos] });
        } else {
            try stderr.print("[X] No matches found for '{s}'\n", .{keyword});
        }
        try stderr.flush(); // Don't forget to flush!
    }

    // Transfer ownership of entries
    var result: types.ManEntryList = .{};
    try result.appendSlice(allocator, searcher.entries.items);

    // Clear the searcher entries so deinit doesn't free them
    searcher.entries.clearRetainingCapacity();

    return result;
}

// Original implementation as fallback using child process
fn searchManPagesOriginal(
    keyword: []const u8,
    target_section: ?[]const []const u8,
    show_paths: bool,
    verbose: bool,
    allocator: std.mem.Allocator,
    stderr: *std.io.Writer,
) !types.ManEntryList {
    _ = show_paths;
    _ = verbose;

    var child = std.process.Child.init(&[_][]const u8{ "man", "-k", keyword }, allocator);
    child.stdout_behavior = .Pipe;
    child.stderr_behavior = .Ignore;

    try child.spawn();

    // In 0.15.1, ArrayList is unmanaged
    var output_list = std.ArrayList(u8){};
    defer output_list.deinit(allocator);

    // Read from child stdout using the new reader interface
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_reader_wrapper = child.stdout.?.reader(&stdout_buffer);
    const reader = &stdout_reader_wrapper.interface;

    // Read all data - use readAll or read in a loop
    var temp_buffer: [4096]u8 = undefined;
    while (true) {
        const bytes_read = reader.takeByte() catch |err| switch (err) {
            error.EndOfStream => break,
            else => return err,
        };
        if (bytes_read == 0) break;
        try output_list.appendSlice(allocator, temp_buffer[0..bytes_read]);
    }

    std.debug.print("ouptut_list len: {d}\n", .{output_list.items.len});

    const term = try child.wait();
    if ((term != .Exited or term.Exited != 0) and output_list.items.len == 0) {
        if (target_section) |sections| {
            // Join sections for display
            var joined: [32]u8 = undefined;
            var pos: usize = 0;
            for (sections, 0..) |s, i| {
                if (i != 0) {
                    joined[pos] = ',';
                    pos += 1;
                }
                @memcpy(joined[pos .. pos + s.len], s);
                pos += s.len;
            }
            try stderr.print("[X] No matches found for '{s}' in section {s}\n", .{ keyword, joined[0..pos] });
        } else {
            try stderr.print("[X] No matches found for '{s}'\n", .{keyword});
        }
        try stderr.flush();
        return .{};
    }

    var entries: types.ManEntryList = .{};
    errdefer {
        for (entries.items) |entry| {
            allocator.free(entry.name);
            allocator.free(entry.section);
            allocator.free(entry.description);
            if (entry.path) |path| allocator.free(path);
        }
        entries.deinit(allocator);
    }

    // Process output line by line
    var line_iterator = std.mem.splitScalar(u8, output_list.items, '\n');
    while (line_iterator.next()) |raw_line| {
        if (raw_line.len == 0) continue;

        var cleaned_buffer: [4096]u8 = undefined;
        var cleaned_len: usize = 0;

        for (raw_line) |byte| {
            if (cleaned_len >= cleaned_buffer.len - 1) break;
            if (byte >= 32 and byte <= 126) {
                cleaned_buffer[cleaned_len] = byte;
                cleaned_len += 1;
            } else if (byte == '\t') {
                cleaned_buffer[cleaned_len] = ' ';
                cleaned_len += 1;
            } else if (byte >= 128) {
                cleaned_buffer[cleaned_len] = '?';
                cleaned_len += 1;
            }
        }

        // const cleaned_line = cleaned_buffer[0..cleaned_len];
        // if (cleaned_line.len == 0) continue;
        //
        // _ = cleaned_line; // Your parsing logic here
    }

    return entries;
}

// Main search function with fallback strategy
pub fn searchManPages(
    keyword: []const u8,
    target_section: ?[]const []const u8,
    show_paths: bool,
    verbose: bool,
    allocator: std.mem.Allocator,
    stderr: *std.Io.Writer, // Post-writergate: concrete type
) !types.ManEntryList {
    // Try fast method first, fallback to man -k if needed
    return searchManPagesFast(keyword, target_section, show_paths, verbose, allocator, stderr) catch |err| {
        if (verbose) {
            try stderr.print("[W] Fast search failed ({}), falling back to man -k...\n", .{err});
            try stderr.flush(); // Don't forget to flush!
        }

        // Fallback to original man -k implementation
        return searchManPagesOriginal(keyword, target_section, show_paths, verbose, allocator, stderr);
    };
}

// ============================================================================
// TESTS
// ============================================================================

test "ManSearcher - basic initialization" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Test the types ManEntryList uses
    var entries = types.ManEntryList.init(allocator);
    defer {
        for (entries.items) |entry| {
            allocator.free(entry.name);
            allocator.free(entry.section);
            allocator.free(entry.description);
        }
        entries.deinit();
    }

    try std.testing.expectEqual(@as(usize, 0), entries.items.len);
}

test "ManEntry - full lifecycle" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const entry = types.ManEntry{
        .name = try allocator.dupe(u8, "grep"),
        .section = try allocator.dupe(u8, "1"),
        .description = try allocator.dupe(u8, "global regular expression print"),
        .path = try allocator.dupe(u8, "/usr/share/man/man1/grep.1.gz"),
    };
    defer {
        allocator.free(entry.name);
        allocator.free(entry.section);
        allocator.free(entry.description);
        if (entry.path) |path| allocator.free(path);
    }

    try std.testing.expectEqualStrings("grep", entry.name);
    try std.testing.expectEqualStrings("1", entry.section);
    try std.testing.expectEqualStrings("global regular expression print", entry.description);
    try std.testing.expect(entry.path != null);
    try std.testing.expectEqualStrings("/usr/share/man/man1/grep.1.gz", entry.path.?);
}

test "ManEntryList - append multiple" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var entries = types.ManEntryList.init(allocator);
    defer {
        for (entries.items) |entry| {
            allocator.free(entry.name);
            allocator.free(entry.section);
            allocator.free(entry.description);
        }
        entries.deinit();
    }

    try entries.append(types.ManEntry{
        .name = try allocator.dupe(u8, "ls"),
        .section = try allocator.dupe(u8, "1"),
        .description = try allocator.dupe(u8, "list"),
        .path = null,
    });

    try entries.append(types.ManEntry{
        .name = try allocator.dupe(u8, "cat"),
        .section = try allocator.dupe(u8, "1"),
        .description = try allocator.dupe(u8, "concatenate"),
        .path = null,
    });

    try std.testing.expectEqual(@as(usize, 2), entries.items.len);
    try std.testing.expectEqualStrings("ls", entries.items[0].name);
    try std.testing.expectEqualStrings("cat", entries.items[1].name);
}
