import RDK, {Data, Response, RioEvent, Schedule, State, Task} from "@retter/rdk";
import z, {ZodObject, ZodRawShape, ZodType} from "zod";

import {KeyValueString} from "@retter/rdk/src";

interface RioMethodProps {
    type?: "READ" | "STATIC" | "WRITE" | "QUEUED_WRITE" // Type of the method. Default is WRITE.
    queryStringModel?: RioValidationModel // Name of the validation model for query strings
    inputModel?: RioValidationModel // Name of the validation model for input body
    outputModel?: RioValidationModel // Name of the validation model for output body
    errorModel?: RioValidationModel // Name of the validation model for output body
    schedule?: string // Schedule rule. It's only available for STATIC methods.
}

export function RioMethod(options?: RioMethodProps) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        let method = descriptor.value!;
        descriptor.value = function () {
            if(options?.inputModel){
                options?.inputModel?.parse(arguments[0].body);
            }
            return method.apply(this, arguments);
        };
    };
}

interface RioClassProps {
    architecture?: 'arm64' | 'x86_64' // Architecture for methods. Default value is arm64 which is faster.
    accelerated?: boolean // Flag to decide whether to cache instances or not
    description?: string // Description to put into the auto-generated documentation.
    privateStateModel?: RioValidationModel // Name of the validation model for query strings
    publicStateModel?: RioValidationModel // Name of the validation model for input body
}

export function RioClass(options?: RioClassProps) {
    return function (target: Function) {
    };
}

export enum RioClassReservedMethods {
    INIT = 'INIT',
    STATE = 'STATE',
    GET = 'GET',
    DESTROY = 'DESTROY',
}

export enum RioReservedUserIdentities {
    DEVELOPER = 'developer',
    ENDUSER = 'enduser',
    NONE = 'none',
    ANONYMOUSUSER = 'anonymous_user',
}

type ClassLookup = { name: string, value: string };

export type RioClassConstructor = string | undefined | { lookup: ClassLookup } | { body: any };

export type RioValidationModel<T extends ZodRawShape = any> = ZodObject<T>;

export type RioMethodModelType<T extends ZodType<any, any, any>> = z.infer<T>;

export class Rio<PrivateState = any, PublicState = any> {

    protected readonly body?: RioClassConstructor;
    protected readonly lookup?: ClassLookup;

    protected instanceId?: string;
    protected newInstance: boolean = true;
    protected state: State<PublicState, PrivateState, any, any> = {};
    protected schedule: Schedule[] = [];
    protected tasks: Task[] = [];
    protected events: RioEvent[] = [];


    constructor(constructorData?: RioClassConstructor) {

        if (constructorData && typeof constructorData === 'string') {
            this.instanceId = constructorData;
            this.newInstance = false;
        } else if (constructorData && typeof constructorData === 'object') {
            if ((constructorData as { lookup: ClassLookup }).lookup) {
                this.lookup = (constructorData as { lookup: ClassLookup }).lookup;
                this.newInstance = false;
            } else if((constructorData as { body: any }).body) {
                this.body = constructorData;
            }
        }

    }

    @RioMethod()
    protected async init(data: Data<any, any, PublicState, PrivateState>): Promise<void> {
    }

    @RioMethod()
    protected async _get(data: Data<any, any, PublicState, PrivateState>): Promise<void> {
        data.response = {statusCode: 200}
    }

    @RioMethod()
    protected async destroy(data: Data<any, any, PublicState, PrivateState>): Promise<void> {
    }

    @RioMethod()
    protected async authorizer(data: Data<any, any, PublicState, PrivateState>): Promise<boolean> {
        if (RioReservedUserIdentities.DEVELOPER) return true;

        switch (data.context.methodName) {
            case RioClassReservedMethods.INIT:
            case RioClassReservedMethods.GET:
            case RioClassReservedMethods.DESTROY:
            case RioClassReservedMethods.STATE:
                if (RioReservedUserIdentities.ENDUSER) return true;
                break;
            default:
                break;
        }
        throw new RioMethodError({
            code: 'PERMISSION_DENIED',
            statusCode: 403,
            message: 'Permission denied',
            title: 'Permission Denied'
        });
    }

    @RioMethod()
    protected async getInstanceId<Input>(data: Data<Input, any, PublicState, PrivateState>): Promise<string> {
        return this.generateInstanceId();
    }

    @RioMethod()
    protected async getState<Input>(data: Data<Input, any, PublicState, PrivateState>): Promise<State<any, any, any, any>> {
        return data.state;
    }

    protected generateInstanceId(): string {
        return BigInt(process.hrtime.bigint()).toString(32);
    }

    /** This method is magical and should not be used directly. **/
    private setDataProperties(data: Data<any, any, PublicState, PrivateState>) {
        this.state = data.state
    }

    /** This method is magical and should not be used directly. **/
    private async getRioClassInstance(): Promise<this> {
        if(this.instanceId || this.lookup) return this;
        const rdk = new RDK();
        const response = await rdk.getInstance({
            classId: this.constructor.name,
            body: this.body,
            instanceId: this.instanceId,
            lookupKey: this.lookup
        })
        if (!response || response.statusCode >= 400 && !response.body && !response.body.instanceId) {
            throw new Error('Failed to get instance');
        }
        this.instanceId = response.body.instanceId;
        this.newInstance = !!(response.body.newInstance as (undefined | boolean));
        return this
    }
}


export interface RioMethodRequest<T = any> {
    httpMethod?: string
    body?: T
    headers?: KeyValueString
    queryStringParams?: Record<string, any>
}


export class RioMethodError extends Error {
    readonly message: string = 'An error occurred!';
    readonly title: string = 'Error';

    private readonly statusCode: number = 400;
    private readonly code?: string;
    private readonly addons?: any;

    constructor(props?: { statusCode?: number, code?: string, message?: string, title?: string, addons?: any }) {
        super();
        if (props?.statusCode) this.statusCode = props.statusCode;
        if (props?.code) this.code = props.code;
        if (props?.message) this.message = props.message;
        if (props?.title) this.title = props.title;
        if (props?.addons) this.addons = props.addons;
    }

    getRioResponse(): Response {
        return {
            statusCode: this.statusCode,
            body: {
                title: this.title,
                message: this.message,
                code: this.code,
                addons: this.addons
            }
        }
    }
}
