const std = @import("std");

const phpz_options = @import("phpz_options");

const c = @import("root.zig").c;
pub const ModuleEntry = c.zend_module_entry;
const errors = @import("errors.zig");
const ini_helper = @import("ini.zig");
const observer = @import("observer.zig");

/// Configuration for creating a PHP extension module.
///
/// This structure defines the metadata and lifecycle hooks for a PHP extension.
/// All lifecycle hooks are optional and will be called by PHP at appropriate times.
/// INI declarations listed in `ini` are collected into Zend definitions and
/// registered during module startup.
/// Class wrappers listed in `classes` are registered during module startup,
/// before `module_startup_fn`.
pub const Config = struct {
    /// Extension name (must be null-terminated).
    /// This should match the filename of your extension (.so file).
    /// Example: "my_extension"
    name: [:0]const u8,

    /// Extension version string (null-terminated).
    /// Displayed in phpinfo() and php -m output.
    /// Default: "0.0.0"
    version: [:0]const u8 = "0.0.0",

    /// Class wrappers to register during module startup, in the given order.
    /// Parent classes and interfaces should appear before children.
    classes: ?[]const type = null,

    /// INI declarations to register during module startup.
    ini: []const type = &.{},

    /// Typed module globals managed by PHP for the extension lifetime.
    /// In ZTS builds, each TSRM thread receives a separate instance.
    globals: ?type = null,

    /// Observer configuration for profiling, error monitoring, and exception tracking.
    /// Each observer type must implement specific methods.
    observer: ?observer.Config = null,

    /// Module startup hook, called when PHP starts (e.g., when Apache starts).
    /// Use this to initialize resources that persist across requests (e.g., register classes).
    /// Called once per PHP process/thread pool initialization.
    ///
    /// If a `register_{name}_symbols` function (generated by gen_stub.php) exists
    /// in the C headers, it is automatically called before this hook.
    module_startup_fn: ?PhpHookFn = null,

    /// Module shutdown hook, called when PHP shuts down (e.g., when Apache stops).
    /// Use this to clean up resources initialized in startup_fn.
    /// Called once per PHP process/thread pool shutdown.
    module_shutdown_fn: ?PhpHookFn = null,

    /// Request startup hook, called at the beginning of each PHP request.
    /// Use this to initialize per-request resources.
    /// Called for every HTTP request or CLI script execution.
    request_startup_fn: ?PhpHookFn = null,

    /// Request shutdown hook, called at the end of each PHP request.
    /// Use this to clean up per-request resources.
    /// Called after every HTTP request or CLI script execution.
    request_shutdown_fn: ?PhpHookFn = null,

    /// phpinfo() hook, called when phpinfo() is invoked.
    /// Use this to display custom information about your extension.
    /// Example: configuration settings, compile-time options, credits.
    info_fn: ?PhpInfoFn = null,
};

pub const PhpInfoFn = fn (*ModuleEntry) void;
pub const PhpHookFn = fn () anyerror!void;

/// Creates and exports a PHP extension module.
///
/// This function creates a PHP module entry structure at compile time and exports
/// the required symbols based on the configuration. For shared library mode, it
/// additionally exports the `get_module` function for the PHP loader.
///
/// Parameters:
///   - cfg: Module configuration including name, version, lifecycle hooks, INI declarations, and optional classes
pub fn module(comptime cfg: Config) void {
    comptime {
        if (!@hasDecl(c, "zend_module_entry")) {
            @compileError("Missing zend_module_entry declaration. Please '#include \"php.h\"'");
        }

        const S = struct {
            var entry: ModuleEntry = createModuleEntry(cfg);
            fn getModule() callconv(.c) *ModuleEntry {
                return &entry;
            }
        };
        @export(&S.entry, .{ .name = cfg.name ++ "_module_entry" });
        if (phpz_options.shared) {
            @export(&S.getModule, .{ .name = "get_module" });
        }
    }
}

fn makePhpHookFn(comptime hook_fn: PhpHookFn) *const fn (c_int, c_int) callconv(.c) c.zend_result {
    return struct {
        fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result {
            _ = module_type;
            _ = module_number;
            hook_fn() catch |err| {
                errors.err(.err, "Module hook function failed: %s", .{@errorName(err).ptr});
                return c.FAILURE;
            };
            return c.SUCCESS;
        }
    }.handle;
}

fn checkModuleClass(comptime Class: type) void {
    if (!@hasDecl(Class, "register")) {
        @compileError(
            "module .classes entry '" ++ @typeName(Class) ++ "' has no register() function; " ++
                "pass the wrapper type returned by phpz.Class()/phpz.SimpleClass(), e.g. .classes = &.{ classes.user.Class }",
        );
    }

    const register_info = @typeInfo(@TypeOf(Class.register));
    if (register_info != .@"fn") {
        @compileError(
            "module .classes entry '" ++ @typeName(Class) ++ "' has a register declaration, but it is not a function; " ++
                "expected pub fn register() void",
        );
    }

    const fn_info = register_info.@"fn";
    if (fn_info.param_types.len != 0 or fn_info.return_type.? != void) {
        @compileError(
            "module .classes entry '" ++ @typeName(Class) ++ "' register declaration must be pub fn register() void; " ++
                "pass the phpz class wrapper type, not the implementation type",
        );
    }
}

fn makePhpModuleStartupFn(
    comptime module_name: [:0]const u8,
    comptime hook_fn: ?PhpHookFn,
    comptime ini_defs: ?[]const c.zend_ini_entry_def,
    comptime classes: ?[]const type,
    comptime observer_cfg: ?observer.Config,
) ?*const fn (c_int, c_int) callconv(.c) c.zend_result {
    const symbols_fn_name = "register_" ++ module_name ++ "_symbols";
    const has_symbols = comptime @hasDecl(c, symbols_fn_name);
    const has_observers = comptime if (observer_cfg) |o| o.fcall != null or o.errors != null or o.exception != null else false;
    if (!has_symbols and hook_fn == null and ini_defs == null and classes == null and !has_observers) return null;
    return struct {
        fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result {
            _ = module_type;
            if (comptime has_symbols) {
                @field(c, symbols_fn_name)(module_number);
            }
            if (comptime ini_defs) |defs| {
                if (c.zend_register_ini_entries(&defs[0], module_number) != c.SUCCESS) {
                    return c.FAILURE;
                }
            }
            if (comptime classes) |items| {
                inline for (items) |Class| {
                    checkModuleClass(Class);
                    Class.register();
                }
            }
            if (comptime observer_cfg) |cfg| {
                observer.startup(cfg);
            }
            if (comptime hook_fn) |f| {
                f() catch |err| {
                    errors.err(.err, "Module startup function failed: %s", .{@errorName(err).ptr});
                    return c.FAILURE;
                };
            }
            return c.SUCCESS;
        }
    }.handle;
}

fn makePhpModuleShutdownFn(
    comptime hook_fn: ?PhpHookFn,
    comptime ini_defs: ?[]const c.zend_ini_entry_def,
    comptime observer_cfg: ?observer.Config,
) ?*const fn (c_int, c_int) callconv(.c) c.zend_result {
    const has_observer_shutdown = comptime if (observer_cfg) |cfg| cfg.exception != null else false;
    if (hook_fn == null and ini_defs == null and !has_observer_shutdown) return null;
    return struct {
        fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result {
            _ = module_type;
            defer {
                if (comptime observer_cfg) |cfg| {
                    observer.shutdown(cfg);
                }
            }
            if (comptime ini_defs != null) {
                c.zend_unregister_ini_entries(module_number);
            }
            if (comptime hook_fn) |f| {
                f() catch |err| {
                    errors.err(.err, "Module shutdown function failed: %s", .{@errorName(err).ptr});
                    return c.FAILURE;
                };
            }
            return c.SUCCESS;
        }
    }.handle;
}

fn makePhpInfoFn(comptime info_fn: PhpInfoFn) *const fn ([*c]ModuleEntry) callconv(.c) void {
    return struct {
        fn handle(entry: [*c]ModuleEntry) callconv(.c) void {
            info_fn(@ptrCast(entry));
        }
    }.handle;
}

inline fn createModuleEntry(comptime cfg: Config) ModuleEntry {
    const ini_defs = ini_helper.collect(cfg.ini);
    const registered_ini_defs: ?[]const c.zend_ini_entry_def = if (cfg.ini.len == 0) null else &ini_defs;

    var entry: ModuleEntry = undefined;

    // STANDARD_MODULE_HEADER
    entry.size = @sizeOf(ModuleEntry);
    entry.zend_api = c.ZEND_MODULE_API_NO;
    entry.zend_debug = c.ZEND_DEBUG;
    entry.zts = c.USING_ZTS;
    entry.ini_entry = null;
    entry.deps = null;

    // EXTENSION SETUP
    entry.name = cfg.name;
    entry.functions = if (@hasDecl(c, "ext_functions")) &c.ext_functions else null;
    entry.module_startup_func = makePhpModuleStartupFn(cfg.name, cfg.module_startup_fn, registered_ini_defs, cfg.classes, cfg.observer);
    entry.module_shutdown_func = makePhpModuleShutdownFn(cfg.module_shutdown_fn, registered_ini_defs, cfg.observer);
    entry.request_startup_func = if (cfg.request_startup_fn) |f| makePhpHookFn(f) else null;
    entry.request_shutdown_func = if (cfg.request_shutdown_fn) |f| makePhpHookFn(f) else null;
    entry.info_func = if (cfg.info_fn) |f| makePhpInfoFn(f) else null;
    entry.version = cfg.version;

    // STANDARD_MODULE_PROPERTIES
    entry.globals_size = 0;
    entry.globals_ctor = null;
    entry.globals_dtor = null;
    entry.post_deactivate_func = null;
    entry.module_started = 0;
    entry.type = 0;
    entry.handle = null;
    entry.module_number = 0;
    entry.build_id = c.phpz_module_build_id();

    if (cfg.globals) |g| {
        g.configure(&entry);
    } else {
        if (c.USING_ZTS == 1) {
            entry.globals_id_ptr = null;
        } else {
            entry.globals_ptr = null;
        }
    }

    return entry;
}

fn checkModuleGlobalsHook(comptime T: type, comptime name: []const u8) void {
    if (!@hasDecl(T, name)) return;

    const hook_info = @typeInfo(@TypeOf(@field(T, name)));
    if (hook_info != .@"fn") {
        @compileError(@typeName(T) ++ "." ++ name ++ " must be a function");
    }

    const fn_info = hook_info.@"fn";
    if (fn_info.param_types.len != 1) {
        @compileError(@typeName(T) ++ "." ++ name ++ " must have the signature fn (*" ++ @typeName(T) ++ ") void");
    }
    const param_type = fn_info.param_types[0] orelse {
        @compileError(@typeName(T) ++ "." ++ name ++ " must have a concrete receiver type");
    };
    const return_type = fn_info.return_type orelse {
        @compileError(@typeName(T) ++ "." ++ name ++ " must have a concrete return type");
    };
    if (param_type != *T or return_type != void) {
        @compileError(@typeName(T) ++ "." ++ name ++ " must have the signature fn (*" ++ @typeName(T) ++ ") void");
    }
}

/// Creates a typed namespace for PHP module globals.
///
/// The globals type must be a non-zero-sized struct whose fields can be
/// initialized with `.{}`. Optional `init(*T) void` and `deinit(*T) void`
/// methods run from PHP's globals constructor and destructor respectively.
pub fn ModuleGlobals(comptime T: type) type {
    comptime {
        if (@typeInfo(T) != .@"struct") {
            @compileError("phpz.ModuleGlobals requires a struct type");
        }
        if (@sizeOf(T) == 0) {
            @compileError("phpz.ModuleGlobals does not support zero-sized globals");
        }
        _ = @as(T, .{});
        checkModuleGlobalsHook(T, "init");
        checkModuleGlobalsHook(T, "deinit");
    }

    if (comptime c.USING_ZTS != 0) {
        return struct {
            var id: c.phpz_rsrc_id = 0;

            /// Returns the current thread's module globals.
            pub inline fn get() *T {
                std.debug.assert(id != 0);
                const ptr = c.phpz_tsrmg_bulk(id) orelse unreachable;
                return @ptrCast(@alignCast(ptr));
            }

            fn construct(raw: ?*anyopaque) callconv(.c) void {
                const value: *T = @ptrCast(@alignCast(raw orelse unreachable));
                value.* = .{};
                if (comptime @hasDecl(T, "init")) value.init();
            }

            fn destruct(raw: ?*anyopaque) callconv(.c) void {
                const value: *T = @ptrCast(@alignCast(raw orelse unreachable));
                if (comptime @hasDecl(T, "deinit")) value.deinit();
            }

            fn configure(entry: *ModuleEntry) void {
                entry.globals_size = @sizeOf(T);
                entry.globals_ctor = &construct;
                entry.globals_dtor = &destruct;
                entry.globals_id_ptr = &id;
            }
        };
    }

    return struct {
        var storage: T = undefined;

        /// Returns the module globals.
        pub inline fn get() *T {
            return &storage;
        }

        fn construct(raw: ?*anyopaque) callconv(.c) void {
            const value: *T = @ptrCast(@alignCast(raw orelse unreachable));
            value.* = .{};
            if (comptime @hasDecl(T, "init")) value.init();
        }

        fn destruct(raw: ?*anyopaque) callconv(.c) void {
            const value: *T = @ptrCast(@alignCast(raw orelse unreachable));
            if (comptime @hasDecl(T, "deinit")) value.deinit();
        }

        fn configure(entry: *ModuleEntry) void {
            entry.globals_size = @sizeOf(T);
            entry.globals_ctor = &construct;
            entry.globals_dtor = &destruct;
            entry.globals_ptr = &storage;
        }
    };
}

test "module globals lifecycle and access types" {
    const TestGlobals = struct {
        count: usize = 40,
        initialized: bool = false,

        pub fn init(self: *@This()) void {
            self.count += 2;
            self.initialized = true;
        }

        pub fn deinit(self: *@This()) void {
            self.count = 0;
            self.initialized = false;
        }
    };
    const Globals = ModuleGlobals(TestGlobals);

    try std.testing.expect(@TypeOf(Globals.get()) == *TestGlobals);

    var value: TestGlobals = undefined;
    Globals.construct(&value);
    try std.testing.expectEqual(@as(usize, 42), value.count);
    try std.testing.expect(value.initialized);

    Globals.destruct(&value);
    try std.testing.expectEqual(@as(usize, 0), value.count);
    try std.testing.expect(!value.initialized);
}

test {
    std.testing.refAllDecls(@This());
}
