import 'reflect-metadata'
import type { HasID } from "./index"

export type Rule = 'required' | 'primary key' | 'unique' | 'email'

export type ValidResult = [boolean, string]

//
// rules

const RULES: Record<string, (value: any) => ValidResult> = {
    required,
    'primary key': primaryKey,
    unique,
    email,
}

function required(value: any): ValidResult {
    return [!!value, "is required"]
}

function unique(value: any): ValidResult {
    // TODO are we checking uniqueness here? might not be worth a db call.
    return [true, "must be unique"]
}

function primaryKey(value: any): ValidResult {
    const errorMsg = "must be an integer"
    try {
        return [required(value)[0] && parseInt(value) > 0, errorMsg]
    } catch (e) {
        return [false, errorMsg]
    }
}

function email(value: any): ValidResult {
    return [/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), "must be a valid email address"]
}

//
// decorator

// rule = arg passed to the decorator
// target = the class constructor
// property = the property name, ex 'id' or 'repoId' or 'name'
export const field = (...rules: Rule[]): PropertyDecorator => {
    return (target: any, property: string | symbol) => {
        if (typeof property !== "string") return
        const type = Reflect.getMetadata('design:type', target, property)
        const ctor = typeof target === "function" ? target : target.constructor
        ctor.__vtypes ??= {}
        ctor.__vtypes[property] ??= type.name
        ctor.__vrules ??= {}
        ctor.__vrules[property] ??= []
        ctor.__vrules[property].push(...rules)
    }
}

//
// validator

// boolean: true if valid, false if invalid
// Record<string, string[]>: key is the property name, value is an array of error messages
export function validate<T extends Record<string, any>>(obj: T): [ok: boolean, errors: Record<string, string[]>] {
    const ctor = obj.constructor as any
    const rules = ctor.__vrules ?? {}
    const errors: Record<string, string[]> = {}

    for (const [prop, list] of Object.entries(rules)) {
        const value = obj[prop as keyof T]
        for (const rule of list as string[]) {
            const [valid, error] = RULES[rule](value)
            if (!valid) {
                errors[prop] ??= []
                errors[prop].push(error)
            }
        }
    }

    return [Object.keys(errors).length == 0, errors]
}

//
// schema generator

const SQLITE_TYPES: Record<string, string> = {
    Number: 'INTEGER',
    String: 'TEXT',
    Boolean: 'BOOLEAN',
}

const SQLITE_RULES: Record<string, string> = {
    'required': 'NOT NULL',
    'primary key': 'PRIMARY KEY AUTOINCREMENT',
    'unique': 'UNIQUE',
}

// Produces a string that can be used to create a SQL table.
export function schema<T extends HasID>(ctor: new (...args: any[]) => T, table: string): string {
    return sqliteSchema(ctor, table)
}

export function sqliteSchema<T extends HasID>(ctor: new (...args: any[]) => T, table: string): string {
    const types: string[] = (ctor as any).__vtypes
    const rules = (ctor as any).__vrules
    const columns: string[] = []

    for (const [prop, type] of Object.entries(types)) {
        let column = `${prop} ${SQLITE_TYPES[type] || 'BLOB'}`
        const propRules = rules[prop]

        if (propRules)
            for (const rule of propRules)
                if (SQLITE_RULES[rule])
                    column += ` ${SQLITE_RULES[rule]}`

        columns.push(column)
    }

    return `CREATE TABLE IF NOT EXISTS ${table} (${columns.join(", ")})`
}

