import {ApiApi as DefaultApi} from "./generated";

export interface ClientAuthProvider {
    /**
     * Abstract method for authenticating a client.
     */
    authenticate(): ClientAuthResponse;
}

export interface ClientAuthConfigurationProvider<T> {
    /**
     * Abstract method for getting the configuration for the client.
     */
    getConfig(): T;
}

export interface ClientAuthCredentialsProvider<T> {
    /**
     * Abstract method for getting the credentials for the client.
     * @param user
     */
    getCredentials(user?: string): T;
}

enum AuthInfoType {
    COOKIE = "cookie",
    HEADER = "header",
    URL = "url",
    METADATA = "metadata"

}

export interface ClientAuthResponse {
    getAuthInfoType(): AuthInfoType;

    getAuthInfo(): { key: string, value: string };
}


export interface AbstractCredentials<T> {
    getCredentials(): T;
}

export interface ClientAuthProtocolAdapter<T> {
    injectCredentials(injectionContext: T): T;

    getApi(): any;
}


class SecretStr {
    constructor(private readonly secret: string) {
    }

    getSecret(): string {
        return this.secret;
    }
}

const base64Encode = (str: string): string => {
    return Buffer.from(str).toString('base64');
};

class BasicAuthCredentials implements AbstractCredentials<SecretStr> {
    private readonly credentials: SecretStr;

    constructor(_creds: string) {
        this.credentials = new SecretStr(base64Encode(_creds))
    }

    getCredentials(): SecretStr {
        //encode base64
        return this.credentials;
    }
}


class BasicAuthClientAuthResponse implements ClientAuthResponse {
    constructor(private readonly credentials: BasicAuthCredentials) {
    }

    getAuthInfo(): { key: string; value: string } {
        return {key: "Authorization", value: "Basic " + this.credentials.getCredentials().getSecret()};
    }

    getAuthInfoType(): AuthInfoType {
        return AuthInfoType.HEADER;
    }
}

export class BasicAuthCredentialsProvider implements ClientAuthCredentialsProvider<BasicAuthCredentials> {
    private readonly credentials: BasicAuthCredentials;

    /**
     * Creates a new BasicAuthCredentialsProvider. This provider loads credentials from provided text credentials or from the environment variable CHROMA_CLIENT_AUTH_CREDENTIALS.
     * @param _creds - The credentials
     * @throws {Error} If neither credentials provider or text credentials are supplied.
     */

    constructor(_creds: string | undefined) {
        if (_creds === undefined && !process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) throw new Error("Credentials must be supplied via environment variable (CHROMA_CLIENT_AUTH_CREDENTIALS) or passed in as configuration.");
        this.credentials = new BasicAuthCredentials((_creds ?? process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) as string);
    }

    getCredentials(): BasicAuthCredentials {
        return this.credentials;
    }
}

class BasicAuthClientAuthProvider implements ClientAuthProvider {
    private readonly credentialsProvider: ClientAuthCredentialsProvider<any>;

    /**
     * Creates a new BasicAuthClientAuthProvider.
     * @param options - The options for the authentication provider.
     * @param options.textCredentials - The credentials for the authentication provider.
     * @param options.credentialsProvider - The credentials provider for the authentication provider.
     * @throws {Error} If neither credentials provider or text credentials are supplied.
     */

    constructor(options: {
        textCredentials: any;
        credentialsProvider: ClientAuthCredentialsProvider<any> | undefined
    }) {
        if (!options.credentialsProvider && !options.textCredentials) {
            throw new Error("Either credentials provider or text credentials must be supplied.");
        }
        this.credentialsProvider = options.credentialsProvider || new BasicAuthCredentialsProvider(options.textCredentials);
    }

    authenticate(): ClientAuthResponse {
        return new BasicAuthClientAuthResponse(this.credentialsProvider.getCredentials());
    }
}

class TokenAuthCredentials implements AbstractCredentials<SecretStr> {
    private readonly credentials: SecretStr;

    constructor(_creds: string) {
        this.credentials = new SecretStr(_creds)
    }

    getCredentials(): SecretStr {
        return this.credentials;
    }
}

export class TokenCredentialsProvider implements ClientAuthCredentialsProvider<TokenAuthCredentials> {
    private readonly credentials: TokenAuthCredentials;

    constructor(_creds: string | undefined) {
        if (_creds === undefined && !process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) throw new Error("Credentials must be supplied via environment variable (CHROMA_CLIENT_AUTH_CREDENTIALS) or passed in as configuration.");
        this.credentials = new TokenAuthCredentials((_creds ?? process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) as string);
    }

    getCredentials(): TokenAuthCredentials {
        return this.credentials;
    }
}

export class TokenClientAuthProvider implements ClientAuthProvider {
    private readonly credentialsProvider: ClientAuthCredentialsProvider<any>;
    private readonly providerOptions: { headerType: TokenHeaderType };

    constructor(options: {
        textCredentials: any;
        credentialsProvider: ClientAuthCredentialsProvider<any> | undefined,
        providerOptions?: { headerType: TokenHeaderType }
    }) {
        if (!options.credentialsProvider && !options.textCredentials) {
            throw new Error("Either credentials provider or text credentials must be supplied.");
        }
        if (options.providerOptions === undefined || !options.providerOptions.hasOwnProperty("headerType")) {
            this.providerOptions = {headerType: "AUTHORIZATION"};
        } else {
            this.providerOptions = {headerType: options.providerOptions.headerType};
        }
        this.credentialsProvider = options.credentialsProvider || new TokenCredentialsProvider(options.textCredentials);
    }

    authenticate(): ClientAuthResponse {
        return new TokenClientAuthResponse(this.credentialsProvider.getCredentials(), this.providerOptions.headerType);
    }

}


type TokenHeaderType = 'AUTHORIZATION' | 'X_CHROMA_TOKEN';

const TokenHeader: Record<TokenHeaderType, (value: string) => { key: string; value: string; }> = {
    AUTHORIZATION: (value: string) => ({key: "Authorization", value: `Bearer ${value}`}),
    X_CHROMA_TOKEN: (value: string) => ({key: "X-Chroma-Token", value: value})
}

class TokenClientAuthResponse implements ClientAuthResponse {
    constructor(private readonly credentials: TokenAuthCredentials, private readonly headerType: TokenHeaderType = 'AUTHORIZATION') {
    }

    getAuthInfo(): { key: string; value: string } {
        if (this.headerType === 'AUTHORIZATION') {
            return TokenHeader.AUTHORIZATION(this.credentials.getCredentials().getSecret());
        } else if (this.headerType === 'X_CHROMA_TOKEN') {
            return TokenHeader.X_CHROMA_TOKEN(this.credentials.getCredentials().getSecret());
        } else {
            throw new Error("Invalid header type: " + this.headerType + ". Valid types are: " + Object.keys(TokenHeader).join(", "));
        }
    }

    getAuthInfoType(): AuthInfoType {
        return AuthInfoType.HEADER;
    }
}


export class IsomorphicFetchClientAuthProtocolAdapter implements ClientAuthProtocolAdapter<RequestInit> {
    authProvider: ClientAuthProvider | undefined;
    wrapperApi: DefaultApi | undefined;

    /**
     * Creates a new adapter of IsomorphicFetchClientAuthProtocolAdapter.
     * @param api - The API to wrap.
     * @param authConfiguration - The configuration for the authentication provider.
     */

    constructor(private api: DefaultApi, authConfiguration: AuthOptions) {

        switch (authConfiguration.provider) {
            case "basic":
                this.authProvider = new BasicAuthClientAuthProvider({
                    textCredentials: authConfiguration.credentials,
                    credentialsProvider: authConfiguration.credentialsProvider
                });
                break;
            case "token":
                this.authProvider = new TokenClientAuthProvider({
                    textCredentials: authConfiguration.credentials,
                    credentialsProvider: authConfiguration.credentialsProvider,
                    providerOptions: authConfiguration.providerOptions
                });
                break;
            default:
                this.authProvider = undefined;
                break;
        }
        if (this.authProvider !== undefined) {
            this.wrapperApi = this.wrapMethods(this.api);
        }
    }

    getApi(): DefaultApi {
        return this.wrapperApi ?? this.api;
    }

    getAllMethods(obj: any): string[] {
        let methods: string[] = [];
        let currentObj = obj;

        do {
            const objMethods = Object.getOwnPropertyNames(currentObj)
                .filter(name => typeof currentObj[name] === 'function' && name !== 'constructor');

            methods = methods.concat(objMethods);
            currentObj = Object.getPrototypeOf(currentObj);
        } while (currentObj);

        return methods;
    }

    wrapMethods(obj: any): any {
        let self = this;
        const methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
            .filter(name => typeof obj[name] === 'function' && name !== 'constructor');

        return new Proxy(obj, {
            get(target, prop: string) {
                if (methodNames.includes(prop)) {
                    return new Proxy(target[prop], {
                        apply(fn, thisArg, args) {
                            const modifiedArgs = args.map(arg => {
                                if (arg && typeof arg === 'object' && 'method' in arg) {
                                    return self.injectCredentials(arg as RequestInit);
                                }
                                return arg;
                            });
                            if (Object.keys(modifiedArgs[modifiedArgs.length - 1]).length === 0) {
                                modifiedArgs[modifiedArgs.length - 1] = self.injectCredentials({} as RequestInit);
                            } else {
                                modifiedArgs[modifiedArgs.length - 1] = self.injectCredentials(modifiedArgs[modifiedArgs.length - 1] as RequestInit);
                            }
                            return fn.apply(thisArg, modifiedArgs);
                        }
                    });
                }
                return target[prop];
            }
        });
    }

    injectCredentials(injectionContext: RequestInit): RequestInit {
        const authInfo = this.authProvider?.authenticate().getAuthInfo();
        if (authInfo) {
            const {key, value} = authInfo;
            injectionContext = {
                ...injectionContext,
                headers: {
                    [key]: value
                },
            }
        }
        return injectionContext;
    }
}


export type AuthOptions = {
    provider: ClientAuthProvider | string | undefined,
    credentialsProvider?: ClientAuthCredentialsProvider<any> | undefined,
    configProvider?: ClientAuthConfigurationProvider<any> | undefined,
    credentials?: any | undefined,
    providerOptions?: any | undefined
}
