const std = @import("std");

// fn UniqueSQLType(comptime ValType: type) type {
//     comptime {
//         _ = switch (@typeInfo(ValType)) {
//             .Int, .Float, .Pointer => true,
//             else => {
//                 @compileError("An invalid type passed to unique sql contraint type: " ++ @typeName(ValType));
//             },
//         };
//     }
//     comptime {
//         const t = switch (@typeInfo(ValType)) {
//             .Int => @Type(.{ .Int = .{ .signedness = @typeInfo(ValType).Int.signedness, .bits = @typeInfo(ValType).Int.bits } }),
//             .Float => @Type(.{ .Float = .{ .bits = @typeInfo(ValType).Float.bits } }),
//             .Pointer => {
//                 const pt = @typeInfo(ValType).Pointer;
//                 // _ = std.fmt.comptimePrint("Pointer type info: {any}", .{pt});
//                 return @Type(.{ .Pointer = .{ .size = pt.size, .is_const = pt.is_const, .is_volatile = pt.is_volatile, .alignment = pt.alignment, .address_space = pt.address_space, .child = pt.child, .is_allowzero = pt.is_allowzero, .sentinel = pt.sentinel } });
//             },
//             else => unreachable,
//         };
//         return t;
//     }
//     // @setEvalBranchQuota(additional_fields.len * fields.len * 10);
// }

// pub const UniqueType = union(enum) { string: []const u8, real: f64 };

pub const Transaction = struct {
    id: u32,
    amount: f64,
    type: []const u8,
    memo: ?[]const u8,
    budget_id: u32,
    created_by_user_id: u32,
    budget_category_id: ?u32,
    date: u64,
    created_at: u64,
    updated_at: u64,
    hide: u8,
};

pub const BudgetCategory = struct {
    id: u32,
    budget_id: u32,
    amount: f64,
    name: []const u8,
    color: []const u8,
    created_at: u64,
    updated_at: u64,
    hide: u8,
};

pub const Budget = struct {
    id: u32,
    family_id: u32,
    name: []const u8,
    created_at: u64,
    updated_at: u64,
    hide: u8,
    expected_income: ?f64,
};

pub const User = struct {
    id: u32,
    name: []const u8,
    username: ?[]const u8,
    email: ?[]const u8,
    pass_hash: u32,
    family_id: u32,
    budget_id: u32,
    created_at: u64,
    updated_at: u64,
    last_activity_at: u64,
    hide: u8,
};

pub const Family = struct {
    id: u32,
    code: ?[]const u8,
    created_at: u64,
    updated_at: u64,
    hide: u8,
};

pub const SharedNote = struct {
    id: u32,
    family_id: u32,
    created_by_user_id: u32,
    content: []const u8,
    title: []const u8,
    color: ?[]const u8,
    tag_ids: []const u8,
    is_markdown: u2,
    created_at: u64,
    updated_at: u64,
    hide: u8,
};

pub const Tag = struct {
    id: u32,
    family_id: u32,
    created_by_user_id: u32,
    name: []const u8,
    type: []const u8,
    created_at: u64,
    updated_at: u64,
    hide: u8,
};

pub const Token = struct {
    user_id: u32,
    family_id: u32,
    generated_at: u64,
    expires_at: u64,
};

pub const ModelTypes = [_]type{ Transaction, BudgetCategory, Budget, User, Family, SharedNote, Tag };

/// Functions for creating SQLite queries for any models above
pub inline fn createSelectOnIdQuery(comptime Type: type) []const u8 {
    return "SELECT * FROM " ++ getTypeTableName(Type) ++ " WHERE id = ?;";
}

pub inline fn createSelectOnFieldQuery(
    comptime Type: type,
    comptime structField: ?std.builtin.Type.StructField,
    comptime fieldName: ?[]const u8,
    comptime comparator: []const u8,
) ![]const u8 {
    comptime {
        if (fieldName == null and structField == null) {
            @compileError("Must provide struct and fieldname");
        } else if (fieldName != null and structField != null) {
            @compileError("Cannot provide struct and fieldname");
        }
        var field: []const u8 = undefined;
        if (structField != null) {
            field = structField.?;
        } else {
            field = fieldName.?;
        }
        var query = "SELECT * FROM " ++ getTypeTableName(Type) ++ " WHERE " ++ field ++ " " ++ comparator ++ " ?;";
        return query;
    }
}

pub inline fn createRawSelectQuery(comptime Type: type, comptime where_query: []const u8) ![]const u8 {
    comptime {
        if (!std.mem.containsAtLeast(u8, where_query, 1, "WHERE")) {
            @compileError("Provided where query does not contain 'WHERE' string: " ++ where_query);
        }
        if (!std.mem.endsWith(u8, where_query, ";")) {
            @compileError("Where query does not end with semicolon: " ++ where_query);
        }
        var query = "SELECT * FROM " ++ getTypeTableName(Type) ++ " " ++ where_query;
        return query;
    }
}

pub inline fn createDeleteOnIdQuery(comptime Type: type) []const u8 {
    return "DELETE from " ++ getTypeTableName(Type) ++ " WHERE id = ?;";
}

pub inline fn createInsertQuery(comptime Type: type) []const u8 {
    comptime {
        var query: []const u8 = "INSERT INTO " ++ getTypeTableName(Type) ++ " (";
        var qs: []const u8 = "?";
        inline for (@typeInfo(Type).Struct.fields, 0..) |field, i| {
            // This is brittle, assumes 'id' struct field is first
            if (std.mem.eql(u8, field.name, "id")) {
                continue;
            }
            if (i > 1) {
                query = query ++ ", ";
                qs = qs ++ ", ?";
            }
            query = query ++ field.name;
        }
        query = query ++ ") VALUES (" ++ qs ++ ");";
        return query;
    }
}

pub inline fn createUpdateQuery(comptime Type: type) []const u8 {
    comptime {
        var query: []const u8 = "UPDATE " ++ getTypeTableName(Type) ++ " SET ";
        inline for (@typeInfo(Type).Struct.fields, 0..) |field, i| {
            if (i != 0) {
                query = query ++ " = ?, ";
            }
            query = query ++ field.name;
        }
        query = query ++ " = ? WHERE id = ?;";
        return query;
    }
}

pub fn createHideQuery(comptime Type: type) []const u8 {
    comptime {
        var has_hide = false;
        inline for (@typeInfo(Type).Struct.fields) |field| {
            if (std.mem.eql(u8, field.name, "hide")) {
                has_hide = true;
            }
        }

        if (!has_hide) {
            @compileError("Model does not have hide field, cannot create hide query: " ++ @typeName(Type));
        }

        return "UPDATE " ++ getTypeTableName(Type) ++ " SET hide = ?, updated_at = ? WHERE id = ?;";
    }
}

pub inline fn createTableDeleteQuery(comptime Type: type) []const u8 {
    return "DROP TABLE IF EXISTS " ++ getTypeTableName(Type) ++ ";";
}

pub inline fn createTableMigrationQuery(comptime Type: type) []const u8 {
    comptime {
        var query: []const u8 = "CREATE TABLE IF NOT EXISTS " ++ getTypeTableName(Type) ++ " (\n";

        inline for (@typeInfo(Type).Struct.fields, 0..) |field, i| {
            if (i != 0) {
                query = query ++ ",\n";
            }
            query = query ++ "\t" ++ field.name ++ " " ++ getSQLiteColumnMigrateText(field);
            //std.debug.print("\nField: {any}\n", .{field});
        }
        query = query ++ "\n) STRICT;";
        return query;
    }
}

inline fn getSQLiteColumnMigrateText(comptime struct_field: std.builtin.Type.StructField) []const u8 {
    comptime {
        if (std.mem.eql(u8, struct_field.name, "id")) return "INTEGER PRIMARY KEY AUTOINCREMENT";
        if (std.mem.eql(u8, @typeName(struct_field.type), "UniqueSQLType")) {
            @compileLog(struct_field.type);
        }

        // _ = std.fmt.comptimePrint("Got type {any}", .{@typeInfo(struct_field.type)});
        const val = switch (@typeInfo(struct_field.type)) {
            .Int => "INTEGER NOT NULL",
            .Float => "REAL NOT NULL",
            .Optional => switch (@typeInfo(@typeInfo(struct_field.type).Optional.child)) {
                .Int => "INTEGER",
                .Float => "REAL",
                .Pointer => "TEXT",
                .Array => "TEXT",
                else => unreachable,
            },
            .Array => "TEXT NOT NULL",
            .Pointer => "TEXT NOT NULL",

            // .Struct => {
            //     if (std.mem.eql(u8, @typeName(struct_field.type), "UniqueSQLString") or std.mem.eql(u8, @typeName(struct_field.type), "UniqueSQLString"))
            //         return "THIS";
            // },
            else => {
                @compileError("Passed in a type that has no sql migration defined: " ++ @typeName(struct_field.type));
            },
        };
        return val;
    }
}

pub inline fn getTypeTableName(comptime Type: type) []const u8 {
    comptime {
        return switch (Type) {
            User => "users",
            Transaction => "transactions",
            Budget => "budgets",
            BudgetCategory => "budget_categories",
            Family => "families",
            SharedNote => "shared_notes",
            Tag => "tags",
            // Tag => "tags",
            else => {
                @compileError("Missing a table name, check to make sure that each model in the models list has a table name provided here.\n" ++ @typeName(Type));
                // _ = std.fmt.comptimePrint("Missing a table name, check to make sure that each model in the models list has a table name provided here.\n{any}", .{ModelTypes});
                // unreachable;
            },
        };
    }
}

test "Comptime Query Builder" {
    var query = createSelectOnIdQuery(Transaction);
    std.debug.print("\nGot SELECT query for Transaction\n\t{s}\n", .{query});
    query = createInsertQuery(Budget);
    std.debug.print("Got INSERT query for Budget\n\t{s}\n", .{query});
    query = createUpdateQuery(BudgetCategory);
    std.debug.print("Got UPDATE query for BudgetCategory\n\t{s}\n", .{query});

    query = createTableMigrationQuery(User);
    std.debug.print("Migration for user table:\n{s}\n", .{query});
}
