const std = @import("std");
const db = @import("../db.zig");
const manifest_lib = @import("../manifest.zig");
const core = @import("../core.zig");
const config = @import("../config.zig");

extern fn execvp(path: [*:0]const u8, argv: [*]const ?[*:0]const u8) c_int;
extern fn waitpid(pid: c_int, status: *c_int, flags: c_int) c_int;

// Get current environment from libc
extern var environ: [*]const ?[*:0]const u8;

// Inline macros for wait status parsing (not available as extern on macOS)
fn WIFEXITED(status: c_int) c_int {
    return if ((status & 0x7F) == 0) 1 else 0;
}
fn WEXITSTATUS(status: c_int) c_int {
    return (status >> 8) & 0xFF;
}
fn WIFSIGNALED(status: c_int) c_int {
    return if (((status & 0x7F) != 0x7F) and ((status & 0x7F) != 0)) 1 else 0;
}
fn WTERMSIG(status: c_int) c_int {
    return status & 0x7F;
}
const Manifest = manifest_lib.RunManifest;

/// Run command - always executes locally
/// Usage:
///   ml run                    # Use entrypoint from config + args
///   ml run --lr 0.001         # Args appended to entrypoint
///   ml run -- python train.py # Explicit command
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
    var flags = core.flags.CommonFlags{};
    var command_args = try core.flags.parseCommon(allocator, args, &flags);
    defer command_args.deinit(allocator);

    core.output.setMode(if (flags.json) .json else .text);

    if (flags.help) {
        return printUsage();
    }

    const cfg = try config.Config.load(allocator);
    defer {
        var mut_cfg = cfg;
        mut_cfg.deinit(allocator);
    }

    // Parse command: entrypoint + args, or explicit -- command
    const command = try resolveCommand(allocator, &cfg, command_args.items);
    defer freeCommand(allocator, command);

    // Generate run_id
    const run_id = try db.generateUUID(allocator);
    defer allocator.free(run_id);

    // Determine experiment name
    const experiment_name = if (cfg.experiment) |exp| exp.name else "default";

    // Build artifact path
    const artifact_path = try std.fs.path.join(allocator, &[_][]const u8{
        cfg.artifact_path,
        experiment_name,
        run_id,
    });
    defer allocator.free(artifact_path);

    // Create run directory
    std.fs.makeDirAbsolute(artifact_path) catch |err| {
        if (err != error.PathAlreadyExists) {
            std.log.err("Failed to create run directory: {}", .{err});
            return error.MkdirFailed;
        }
    };

    // Get DB path and initialize if needed (lazy bootstrap)
    const db_path = try cfg.getDBPath(allocator);
    defer allocator.free(db_path);

    var database = try initOrOpenDB(allocator, db_path);
    defer database.close();

    // Write run manifest (status=RUNNING)
    const manifest_path = try std.fs.path.join(allocator, &[_][]const u8{
        artifact_path,
        "run_manifest.json",
    });
    defer allocator.free(manifest_path);

    const timestamp = try db.currentTimestamp(allocator);
    defer allocator.free(timestamp);

    var manifest = Manifest.init(allocator);
    manifest.run_id = run_id;
    manifest.experiment = experiment_name;
    manifest.command = try std.mem.join(allocator, " ", command);
    manifest.args = try duplicateStrings(allocator, command);
    manifest.started_at = try allocator.dupe(u8, timestamp);
    manifest.status = "RUNNING";
    manifest.artifact_path = artifact_path;
    manifest.synced = false;

    // Insert run into database
    const run_name = try std.fmt.allocPrint(allocator, "run-{s}", .{run_id[0..8]});
    defer allocator.free(run_name);

    const sql = "INSERT INTO ml_runs (run_id, experiment_id, name, status, start_time, synced) VALUES (?, ?, ?, 'RUNNING', ?, 0);";
    const stmt = try database.prepare(sql);
    defer db.DB.finalize(stmt);

    try db.DB.bindText(stmt, 1, run_id);
    try db.DB.bindText(stmt, 2, experiment_name);
    try db.DB.bindText(stmt, 3, run_name);
    try db.DB.bindText(stmt, 4, timestamp);
    _ = try db.DB.step(stmt);

    // Write manifest
    try manifest_lib.writeManifest(manifest, manifest_path, allocator);

    // Fork and execute
    const output_log_path = try std.fs.path.join(allocator, &[_][]const u8{
        artifact_path,
        "output.log",
    });
    defer allocator.free(output_log_path);

    // Execute and capture
    const exit_code = try executeAndCapture(
        allocator,
        command,
        output_log_path,
        &database,
        run_id,
    );

    // Update run status in database
    const end_time = try db.currentTimestamp(allocator);
    defer allocator.free(end_time);

    const status = if (exit_code == 0) "FINISHED" else "FAILED";

    const update_sql = "UPDATE ml_runs SET status = ?, end_time = ?, exit_code = ?, pid = NULL WHERE run_id = ?;";
    const update_stmt = try database.prepare(update_sql);
    defer db.DB.finalize(update_stmt);

    try db.DB.bindText(update_stmt, 1, status);
    try db.DB.bindText(update_stmt, 2, end_time);
    try db.DB.bindInt64(update_stmt, 3, exit_code);
    try db.DB.bindText(update_stmt, 4, run_id);
    _ = try db.DB.step(update_stmt);

    // Update manifest
    try manifest_lib.updateManifestStatus(manifest_path, status, exit_code, allocator);

    // Checkpoint WAL
    database.checkpointOnExit();

    // Print result
    if (flags.json) {
        std.debug.print("{{\"success\":true,\"run_id\":\"{s}\",\"status\":\"{s}\",\"exit_code\":{d}}}\n", .{
            run_id,
            status,
            exit_code,
        });
    } else {
        std.debug.print("[OK] Run {s} complete ({s})\n", .{ run_id[0..8], status });
        if (cfg.sync_uri.len > 0) {
            std.debug.print("-> queued for sync\n", .{});
        }
    }
}

/// Resolve command from entrypoint + args, or explicit -- command
fn resolveCommand(allocator: std.mem.Allocator, cfg: *const config.Config, args: []const []const u8) ![][]const u8 {
    // Check for explicit -- separator
    var double_dash_idx: ?usize = null;
    for (args, 0..) |arg, i| {
        if (std.mem.eql(u8, arg, "--")) {
            double_dash_idx = i;
            break;
        }
    }

    if (double_dash_idx) |idx| {
        // Explicit command after --
        if (idx + 1 >= args.len) {
            std.log.err("No command provided after --", .{});
            return error.NoCommand;
        }
        return try allocator.dupe([]const u8, args[idx + 1 ..]);
    }

    // Use entrypoint from config + args
    if (cfg.experiment) |exp| {
        if (exp.entrypoint.len > 0) {
            // Split entrypoint on spaces
            var argv: std.ArrayList([]const u8) = .empty;

            // Parse entrypoint (split on spaces)
            var iter = std.mem.splitScalar(u8, exp.entrypoint, ' ');
            while (iter.next()) |part| {
                if (part.len > 0) {
                    try argv.append(allocator, try allocator.dupe(u8, part));
                }
            }

            // Append args
            for (args) |arg| {
                try argv.append(allocator, try allocator.dupe(u8, arg));
            }

            return try argv.toOwnedSlice(allocator);
        }
    }

    // No entrypoint configured and no explicit command
    std.log.err("No entrypoint configured. Set entrypoint in .fetchml/config.toml or use: ml run -- <command>", .{});
    return error.NoEntrypoint;
}

/// Free command array
fn freeCommand(allocator: std.mem.Allocator, command: [][]const u8) void {
    for (command) |arg| {
        allocator.free(arg);
    }
    allocator.free(command);
}

/// Duplicate array of strings
fn duplicateStrings(allocator: std.mem.Allocator, strings: []const []const u8) ![][]const u8 {
    const result = try allocator.alloc([]const u8, strings.len);
    for (strings, 0..) |s, i| {
        result[i] = try allocator.dupe(u8, s);
    }
    return result;
}

/// Initialize or open database (lazy bootstrap)
fn initOrOpenDB(allocator: std.mem.Allocator, db_path: []const u8) !db.DB {
    const db_exists = blk: {
        std.fs.accessAbsolute(db_path, .{}) catch |err| {
            if (err == error.FileNotFound) break :blk false;
        };
        break :blk true;
    };

    const database = try db.DB.init(allocator, db_path);

    if (!db_exists) {
        std.log.info("local mode active — tracking to {s}", .{db_path});
    }

    return database;
}

/// Execute command and capture output, parsing FETCHML_METRIC lines
fn executeAndCapture(
    allocator: std.mem.Allocator,
    command: []const []const u8,
    output_path: []const u8,
    database: *db.DB,
    run_id: []const u8,
) !i32 {
    // Create output file
    var output_file = try std.fs.cwd().createFile(output_path, .{});
    defer output_file.close();

    // Create pipe for stdout
    const pipe = try std.posix.pipe();
    defer {
        std.posix.close(pipe[0]);
        std.posix.close(pipe[1]);
    }

    // Fork child process
    const pid = try std.posix.fork();

    if (pid == 0) {
        // Child process
        std.posix.close(pipe[0]); // Close read end

        // Redirect stdout to pipe
        _ = std.posix.dup2(pipe[1], std.posix.STDOUT_FILENO) catch std.process.exit(1);
        _ = std.posix.dup2(pipe[1], std.posix.STDERR_FILENO) catch std.process.exit(1);
        std.posix.close(pipe[1]);

        // Execute command using execvp (uses current environ)
        const c_err = execvp(@ptrCast(command[0].ptr), @ptrCast(command.ptr));
        std.log.err("Failed to execute {s}: {}", .{ command[0], c_err });
        std.process.exit(1);
        unreachable;
    }

    // Parent process
    std.posix.close(pipe[1]); // Close write end

    // Store PID in database
    const pid_sql = "UPDATE ml_runs SET pid = ? WHERE run_id = ?;";
    const pid_stmt = try database.prepare(pid_sql);
    defer db.DB.finalize(pid_stmt);
    try db.DB.bindInt64(pid_stmt, 1, pid);
    try db.DB.bindText(pid_stmt, 2, run_id);
    _ = try db.DB.step(pid_stmt);

    // Read from pipe and parse FETCHML_METRIC lines
    var buf: [4096]u8 = undefined;
    var line_buf: [1024]u8 = undefined;
    var line_len: usize = 0;

    while (true) {
        const bytes_read = std.posix.read(pipe[0], &buf) catch |err| {
            if (err == error.WouldBlock or err == error.BrokenPipe) break;
            break;
        };

        if (bytes_read == 0) break;

        // Write to output file
        try output_file.writeAll(buf[0..bytes_read]);

        // Parse lines
        for (buf[0..bytes_read]) |byte| {
            if (byte == '\n' or line_len >= line_buf.len - 1) {
                if (line_len > 0) {
                    line_buf[line_len] = 0;
                    const line = line_buf[0..line_len];
                    try parseAndLogMetric(allocator, line, database, run_id);
                    line_len = 0;
                }
            } else {
                line_buf[line_len] = byte;
                line_len += 1;
            }
        }
    }

    // Wait for child
    var status: c_int = 0;
    _ = waitpid(@intCast(pid), &status, 0);

    // Parse exit code
    if (WIFEXITED(status) != 0) {
        return WEXITSTATUS(status);
    } else if (WIFSIGNALED(status) != 0) {
        return 128 + WTERMSIG(status);
    }

    return -1;
}

/// Parse FETCHML_METRIC line and log to database
/// Format: FETCHML_METRIC key=value [step=N]
fn parseAndLogMetric(
    allocator: std.mem.Allocator,
    line: []const u8,
    database: *db.DB,
    run_id: []const u8,
) !void {
    const trimmed = std.mem.trim(u8, line, " \t\r");

    // Check prefix
    const prefix = "FETCHML_METRIC";
    if (!std.mem.startsWith(u8, trimmed, prefix)) return;

    // Get the rest after prefix
    var rest = trimmed[prefix.len..];
    rest = std.mem.trimLeft(u8, rest, " \t");

    // Parse key=value
    var iter = std.mem.splitScalar(u8, rest, ' ');
    const kv_part = iter.next() orelse return;

    var kv_iter = std.mem.splitScalar(u8, kv_part, '=');
    const key = kv_iter.next() orelse return;
    const value_str = kv_iter.next() orelse return;

    // Validate key: [a-zA-Z][a-zA-Z0-9_]*
    if (key.len == 0) return;
    const first_char = key[0];
    if (!std.ascii.isAlphabetic(first_char)) return;
    for (key[1..]) |c| {
        if (!std.ascii.isAlphanumeric(c) and c != '_') return;
    }

    // Parse value
    const value = std.fmt.parseFloat(f64, value_str) catch return;

    // Parse optional step
    var step: i64 = 0;
    while (iter.next()) |part| {
        if (std.mem.startsWith(u8, part, "step=")) {
            const step_str = part[5..];
            step = std.fmt.parseInt(i64, step_str, 10) catch 0;
            if (step < 0) step = 0;
        }
    }

    // Insert metric
    const sql = "INSERT INTO ml_metrics (run_id, key, value, step) VALUES (?, ?, ?, ?);";
    const stmt = try database.prepare(sql);
    defer db.DB.finalize(stmt);

    try db.DB.bindText(stmt, 1, run_id);
    try db.DB.bindText(stmt, 2, key);
    try db.DB.bindDouble(stmt, 3, value);
    try db.DB.bindInt64(stmt, 4, step);
    _ = try db.DB.step(stmt);

    _ = allocator;
}

fn printUsage() !void {
    std.debug.print("Usage: ml run [options] [args...]\n", .{});
    std.debug.print("\t\t\tml run -- <command> [args...]\n\n", .{});
    std.debug.print("Execute a run locally with experiment tracking.\n\n", .{});
    std.debug.print("Options:\n", .{});
    std.debug.print("\t--help, -h\tShow this help message\n", .{});
    std.debug.print("\t--json\t\tOutput structured JSON\n\n", .{});
    std.debug.print("Examples:\n", .{});
    std.debug.print("\tml run\t\t\t# Use entrypoint from config\n", .{});
    std.debug.print("\tml run --lr 0.001\t\t# Append args to entrypoint\n", .{});
    std.debug.print("\tml run -- python train.py\t# Run explicit command\n", .{});
}
