import { Scope } from "../../types";

export type body = Record<string, boolean | string | number | null>;
export type fetchOption = Record<string, boolean | string | number | null>;

export type Params = Record<string, number | string>;

export interface PaginationResponse<T> {
    pagination: {
        more: boolean;
        items_per_page: number;
    };
    results: T[];
    count: number;
}

export type FetchParams = Record<string, string | number | null>;

export interface TransportExceptionInterface {
    name: string;
}

export interface ValidationExceptionInterface
    extends TransportExceptionInterface {
    name: "ValidationException";
    error: object;
    violations: string[];
    titles: string[];
    propertyPaths: string[];
}

export interface ValidationErrorResponse extends TransportExceptionInterface {
    violations: {
        title: string;
        propertyPath: string;
    }[];
}

export interface AccessExceptionInterface extends TransportExceptionInterface {
    name: "AccessException";
    violations: string[];
}

export interface NotFoundExceptionInterface
    extends TransportExceptionInterface {
    name: "NotFoundException";
}

export interface ServerExceptionInterface extends TransportExceptionInterface {
    name: "ServerException";
    message: string;
    code: number;
    body: string;
}

export interface ConflictHttpExceptionInterface
    extends TransportExceptionInterface {
    name: "ConflictHttpException";
    violations: string[];
}

/**
 * Generic api method that can be adapted to any fetch request
 */
export const makeFetch = <Input, Output>(
    method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
    url: string,
    body?: body | Input | null,
    options?: FetchParams,
): Promise<Output> => {
    let opts = {
        method: method,
        headers: {
            "Content-Type": "application/json;charset=utf-8",
        },
    };

    if (body !== null && typeof body !== "undefined") {
        Object.assign(opts, { body: JSON.stringify(body) });
    }

    if (typeof options !== "undefined") {
        opts = Object.assign(opts, options);
    }
    return fetch(url, opts).then((response) => {
        if (response.status === 204) {
            return Promise.resolve();
        }

        if (response.ok) {
            return response.json();
        }

        if (response.status === 422) {
            return response.json().then((response) => {
                throw ValidationException(response);
            });
        }

        if (response.status === 403) {
            throw AccessException(response);
        }

        if (response.status === 409) {
            throw ConflictHttpException(response);
        }

        throw {
            name: "Exception",
            sta: response.status,
            txt: response.statusText,
            err: new Error(),
            violations: response.body,
        };
    });
};

/**
 * Fetch results with certain parameters
 */
function _fetchAction<T>(
    page: number,
    uri: string,
    params?: FetchParams,
): Promise<PaginationResponse<T>> {
    const item_per_page = 50;

    const searchParams = new URLSearchParams();
    searchParams.append("item_per_page", item_per_page.toString());
    searchParams.append("page", page.toString());

    if (params !== undefined) {
        Object.keys(params).forEach((key) => {
            const v = params[key];
            if (typeof v === "string") {
                searchParams.append(key, v);
            } else if (typeof v === "number") {
                searchParams.append(key, v.toString());
            } else if (v === null) {
                searchParams.append(key, "");
            }
        });
    }

    const url = uri + "?" + searchParams.toString();

    return fetch(url, {
        method: "GET",
        headers: {
            "Content-Type": "application/json;charset=utf-8",
        },
    })
        .then((response) => {
            if (response.ok) {
                return response.json();
            }

            if (response.status === 404) {
                throw NotFoundException(response);
            }

            if (response.status === 422) {
                return response.json().then((response) => {
                    throw ValidationException(response);
                });
            }

            if (response.status === 403) {
                throw AccessException(response);
            }

            if (response.status >= 500) {
                return response.text().then((body) => {
                    throw ServerException(response.status, body);
                });
            }

            throw new Error("other network error");
        })
        .catch(
            (
                reason:
                    | NotFoundExceptionInterface
                    | ServerExceptionInterface
                    | ValidationExceptionInterface
                    | TransportExceptionInterface,
            ) => {
                console.error(reason);
                throw reason;
            },
        );
}

export const fetchResults = async <T>(
    uri: string,
    params?: FetchParams,
): Promise<T[]> => {
    const promises: Promise<T[]>[] = [];
    let page = 1;
    const firstData: PaginationResponse<T> = (await _fetchAction(
        page,
        uri,
        params,
    )) as PaginationResponse<T>;

    promises.push(Promise.resolve(firstData.results));

    if (firstData.pagination.more) {
        do {
            page = ++page;
            promises.push(
                _fetchAction<T>(page, uri, params).then((r) =>
                    Promise.resolve(r.results),
                ),
            );
        } while (page * firstData.pagination.items_per_page < firstData.count);
    }

    return Promise.all(promises).then((values) => values.flat());
};

export const fetchScopes = (): Promise<Scope[]> => {
    return fetchResults("/api/1.0/main/scope.json");
};

/**
 * Error objects to be thrown
 */
const ValidationException = (
    response: ValidationErrorResponse,
): ValidationExceptionInterface => {
    const error = {} as ValidationExceptionInterface;
    error.name = "ValidationException";
    error.violations = response.violations.map(
        (violation) => `${violation.title}: ${violation.propertyPath}`,
    );
    error.titles = response.violations.map((violation) => violation.title);
    error.propertyPaths = response.violations.map(
        (violation) => violation.propertyPath,
    );
    return error;
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const AccessException = (response: Response): AccessExceptionInterface => {
    const error = {} as AccessExceptionInterface;
    error.name = "AccessException";
    error.violations = ["You are not allowed to perform this action"];

    return error;
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
    const error = {} as NotFoundExceptionInterface;
    error.name = "NotFoundException";

    return error;
};

const ServerException = (
    code: number,
    body: string,
): ServerExceptionInterface => {
    const error = {} as ServerExceptionInterface;
    error.name = "ServerException";
    error.code = code;
    error.body = body;

    return error;
};

const ConflictHttpException = (
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    response: Response,
): ConflictHttpExceptionInterface => {
    const error = {} as ConflictHttpExceptionInterface;

    error.name = "ConflictHttpException";
    error.violations = [
        "Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again",
    ];

    return error;
};
