import { Discipline } from '../discipline';
import { BimIfcClass } from '../bim-ifc-class';
import {
    BimContainer,
    BimContainerInfo,
    BimChangeType,
    Lcid,
    BimChangeIfc,
    TwinfinityInfo,
    BimChange,
    BimUserInfo,
    IfcMetadata,
    BimChangeDwg,
    DwgMetadata2D
} from '../bim-api-client';
import { HttpResponseType, TypedResponse, createMockTypedResponse } from '../../http';
import { TwinfinityApiClient } from './TwinfinityApiClient';
import { LayerApiClient } from './LayerApiClient';
import { MessageApiClient } from './MessageApiClient';
import { UploadApiClient } from './UploadApiClient';
import { OwnerSystem } from '../OwnerSystem';
import { SettingsApiClient } from './SettingsApiClient';
import { BimUserSettingsApiClient } from './BimUserSettingsApiClient';
import { LayerDefinitions } from '../LayerDefinitions';
import { Permissions } from '../Permission';
import { IfcSiteInformationImplementation } from '../IfcSiteInformation';
import { Writeable } from '../../Types';
import { MapBoxApiClient } from './MapBoxApiClient';
import { PredefinedBimChangeMetadataQuery } from '../PredefinedBimChangeMetadataQuery';
import type { ITwinClient } from '@twinfinity/twin';

/**
 * Loads container and IFC data from local files. Mostly used for dev purposes.
 * @hidden
 */
export class BimDevApiClient implements TwinfinityApiClient {
    private readonly _rootDummyProjectId = 'root';
    private readonly _anchorElement = document.createElement('a');
    public readonly layers: LayerApiClient;
    public readonly messages: MessageApiClient;
    public readonly upload: UploadApiClient;
    public readonly settings: SettingsApiClient;
    public readonly mapBox: MapBoxApiClient;
    public readonly twins: ITwinClient;

    public readonly id = 'dev-api-client';
    private readonly _mockUser = {
        id: '1',
        name: 'Pelle Fake',
        firstName: 'Pelle',
        lastName: 'Fake',
        privacyPolicyUrl: 'http://foo.com/privacyPolicy',
        email: 'pellefake@faker.com'
    };

    constructor(private containersAndFiles: { [key: string]: string[] }) {
        const notImplemented = (): any => {
            throw new Error('Not implemented');
        };
        // TODO: Create own dev class when we actually need to have implemented
        // methods
        this.layers = {
            update: notImplemented,
            add: notImplemented
        };

        this.messages = {
            sendMailMessage: notImplemented
        };

        this.upload = {
            createFileSession: notImplemented,
            canAppendFileVersion: notImplemented,
            canCreateFileInFolder: notImplemented,
            canCreateFileIn: notImplemented
        };

        this.settings = {
            user: new BimUserSettingsApiClient('')
        };

        this.mapBox = {
            getAccessToken: notImplemented
        };

        this.twins = {
            listTwins: notImplemented,
            listAllTwins: notImplemented,
            getTwin: notImplemented,
            listTwinVersions: notImplemented,
            downloadTwinContent: notImplemented,
            loadTwin: notImplemented,
            loadTwinWithFactories: notImplemented,
            loadGeometryInfo: notImplemented,
            loadGeometryInfoWithFactories: notImplemented,
            loadTwinWithGeometry: notImplemented,
            getTwinDiagnostics: notImplemented,
            getTwinVersionDiagnostics: notImplemented,
            createOrUpdateTwin: notImplemented,
            updateTwin: notImplemented,
            restoreTwin: notImplemented,
            getTwinVersionContent: notImplemented,
            getTwinContent: notImplemented,
            getTwinVersionEvents: notImplemented,
            getProcessorEvents: notImplemented,
            listAssets: notImplemented,
            getAsset: notImplemented,
            createAssets: notImplemented,
            updateAsset: notImplemented,
            deleteAsset: notImplemented,
            restoreAsset: notImplemented,
            batchAssets: notImplemented,
            batchGetAssets: notImplemented,
            listTwinFiles: notImplemented,
            getTwinFile: notImplemented,
            createOrUpdateTwinFile: notImplemented,
            deleteTwinFile: notImplemented,
            restoreTwinFile: notImplemented,
            getTwinFileContent: notImplemented,
            getTwinFileVersion: notImplemented,
            listTwinFileVersions: notImplemented,
            getTwinFileVersionContent: notImplemented,
            deleteTwin: notImplemented
        };
    }

    public canDelete(change: BimChange): boolean {
        return false;
    }

    public deleteChanges(changesToDelete: Pick<BimChange, 'id' | 'etag'>[]): Promise<TypedResponse<number>[]> {
        throw new Error('not implemented');
    }

    public getUserInfo(): Promise<TypedResponse<Omit<BimUserInfo, 'id'>>> {
        return Promise.resolve(createMockTypedResponse({ value: this._mockUser }));
    }

    /** See {@link TwinfinityApiClient.getContainerInfo} */
    public getContainerInfo(containerOrUrl?: BimContainer | URL): Promise<TypedResponse<BimContainerInfo>> {
        let containerId: string | null = this._rootDummyProjectId;
        if (containerOrUrl) {
            if ('url' in containerOrUrl) {
                containerOrUrl = new URL(containerOrUrl.url);
            }
            containerId = this.getContainerId(containerOrUrl);
            if (!containerId) {
                throw new Error(`${containerOrUrl.href} does not exist.`);
            }
        }

        return Promise.resolve(
            createMockTypedResponse({
                value: {
                    id: containerId,
                    title: containerId,
                    language: 'en-US',
                    info: {
                        title: containerId,
                        language: 'en-US'
                    },
                    user: this._mockUser
                }
            })
        );
    }

    /** See {@link TwinfinityApiClient.getContainers} */
    public async getContainers(id?: string): Promise<BimContainer[]> {
        let ret: BimContainer[] = Object.keys(this.containersAndFiles).map((id) => {
            const serverRelativeUrl = '?container=' + id;
            const url = this.makeAbsoluteUrl(serverRelativeUrl).toString();
            return {
                type: BimChangeType.Container,
                ownerSystem: OwnerSystem.twinfinity.id,
                containerId: '-',
                url: this.makeAbsoluteUrl(serverRelativeUrl).toString(),
                path: this.makeAbsoluteUrl(serverRelativeUrl).pathname,
                id: id,
                format: 'web',
                name: id,
                version: 0,
                metadata: {},
                language: 'en-US',
                lcid: Lcid.Swedish,
                modifiedUtc: new Date(),
                hasGeometry: true,
                serverRelativeUrl: serverRelativeUrl,
                apiUrl: url.toString(),
                containerType: 'ps',
                etag: '123',
                permissionsString: Permissions.join('view').toString(),
                permissions: new Permissions('view'),
                availableLayerDefinitions: new LayerDefinitions()
            };
        });
        if (id !== undefined) {
            ret = ret.filter((c) => c.id === id);
        }
        return Promise.resolve(ret);
    }

    // TODO Add some kind of filter here so users can decide what type of files to get
    // perhaps filtered on metadata or similar. Also allows us to get specific files etc etc
    // graphql might be a good fit for these kind of things. If we do that we might not need
    // functions for different types of changes (diagnostics etc).
    // Initially we can use some kind of jsonb to perform the search?
    /** See {@link TwinfinityApiClient.getIfcChanges} */
    public async getIfcChanges(containerOrUrl: BimContainer | URL, olderThan?: Date): Promise<BimChangeIfc[]> {
        return (await this.getChanges<BimChangeIfc>(containerOrUrl, PredefinedBimChangeMetadataQuery.ifc())).value;
    }

    public async getChanges<T extends BimChange = BimChange>(
        containerOrUrl: BimContainer | URL,
        options: { id: string } | { query: string }
    ): Promise<TypedResponse<T[]>> {
        if ('url' in containerOrUrl) {
            if (containerOrUrl.containerId === this._rootDummyProjectId) {
                return createMockTypedResponse<T[]>({ value: [] });
            }
            containerOrUrl = new URL(containerOrUrl.url);
        }

        const containerId = this.getContainerId(containerOrUrl);
        if (!containerId) {
            throw new Error('Missing ?container=<name> on query string');
        }
        const fileNames = this.containersAndFiles[containerId];

        const ret: T[] = [];

        const shouldLoadIfc = !('query' in options) || options.query === '' || options.query.startsWith('ifc');
        const shouldLoadDwg = !('query' in options) || options.query === '' || options.query.startsWith('dwg');

        for (const fn of fileNames) {
            const apiUrl = this.makeAbsoluteUrl(`dist/${containerId}/`);
            const fakeUrl = new URL(`${fn}?_r=${fn}`.replace(/^\/+/, ''), apiUrl);
            const metadataResponse = await this.get<any>(
                fakeUrl.origin + fakeUrl.pathname + '.metadata' + fakeUrl.search + fakeUrl.hash,
                HttpResponseType.json
            );
            if (shouldLoadIfc && fn.endsWith('.ifc')) {
                const metadata: Writeable<IfcMetadata> =
                    metadataResponse.status === 200 ? await metadataResponse.value : {};

                metadata.sites = (metadata.sites ?? []).map((site) => new IfcSiteInformationImplementation(site));

                const ifcChange: BimChangeIfc = {
                    type: BimChangeType.Ifc,
                    ownerSystem: OwnerSystem.twinfinity.id,
                    url: fakeUrl.toString(),
                    path: fakeUrl.pathname,
                    containerId: containerId,
                    blobId: 'blob_' + fn,
                    id: fn,
                    format: 'ifc',
                    discipline: Discipline.getOrAddFromServerRelativeUrl(fakeUrl.pathname),
                    productCount: metadata.statistics?.productCount ?? -1,
                    name: fn,
                    version: 0,
                    etag: '123',
                    metadata: { ifc: metadata, serverRelativeUrl: apiUrl.pathname },
                    floors: metadata.floors ?? [],
                    classes: (metadata.classes ?? []).map((c: string) => BimIfcClass.getOrAdd(c)),
                    permissionsString: Permissions.join('view', 'add').toString(),
                    permissions: new Permissions('view', 'add'),
                    apiUrl: apiUrl.toString(),
                    resourceUrl: {
                        idx: new URL(fakeUrl.origin + fakeUrl.pathname + '.idx' + fakeUrl.search + fakeUrl.hash),
                        geom: new URL(fakeUrl.origin + fakeUrl.pathname + '.geom' + fakeUrl.search + fakeUrl.hash),
                        t8cm: new URL(fakeUrl.origin + fakeUrl.pathname + '.t8cm' + fakeUrl.search + fakeUrl.hash),
                        t8gi: new URL(fakeUrl.origin + fakeUrl.pathname + '.t8gi' + fakeUrl.search + fakeUrl.hash),
                        t8gd: new URL(fakeUrl.origin + fakeUrl.pathname + '.t8gd' + fakeUrl.search + fakeUrl.hash),
                        'geom-feat': new URL(
                            fakeUrl.origin + fakeUrl.pathname + '.geom-feat' + fakeUrl.search + fakeUrl.hash
                        ),
                        prop: new URL(fakeUrl.origin + fakeUrl.pathname + '.prop' + fakeUrl.search + fakeUrl.hash)
                    },
                    availableLayerDefinitions: new LayerDefinitions()
                };

                if (!(await fetch(ifcChange.resourceUrl.t8cm!, { method: 'HEAD' })).ok) {
                    (ifcChange.resourceUrl as any).t8cm = undefined;
                }
                if (!(await fetch(ifcChange.resourceUrl.t8gi!, { method: 'HEAD' })).ok) {
                    (ifcChange.resourceUrl as any).t8gi = undefined;
                }
                if (!(await fetch(ifcChange.resourceUrl.t8gd!, { method: 'HEAD' })).ok) {
                    (ifcChange.resourceUrl as any).t8gd = undefined;
                }

                ret.push(ifcChange as T);
            } else if (shouldLoadDwg && fn.endsWith('.dwg')) {
                const metadata: Writeable<DwgMetadata2D> =
                    metadataResponse.status === 200 ? await metadataResponse.value : {};

                metadata.url = { svg: fakeUrl.origin + fakeUrl.pathname + '.svg' + fakeUrl.search + fakeUrl.hash };
                const ifcChange: BimChangeDwg = {
                    type: BimChangeType.Dwg,
                    ownerSystem: OwnerSystem.twinfinity.id,
                    url: fakeUrl.toString(),
                    path: fakeUrl.pathname,
                    containerId: containerId,
                    blobId: 'blob_' + fn,
                    id: fn,
                    format: 'ifc',
                    discipline: Discipline.getOrAddFromServerRelativeUrl(fakeUrl.pathname),
                    name: fn,
                    version: 0,
                    etag: '123',
                    metadata: { dwg: metadata, serverRelativeUrl: apiUrl.pathname },
                    permissionsString: Permissions.join('view', 'add').toString(),
                    permissions: new Permissions('view', 'add'),
                    apiUrl: apiUrl.toString(),
                    availableLayerDefinitions: new LayerDefinitions()
                };
                ret.push(ifcChange as T);
            }
        }

        return createMockTypedResponse<T[]>({ value: ret });
    }

    /** See {@link TwinfinityApiClient.get} */
    public async get<T>(
        absoluteOrRelativeUrl: URL | string,
        converter: (r: Response) => TypedResponse<T>,
        init?: RequestInit
    ): Promise<TypedResponse<T>> {
        const response = await fetch(absoluteOrRelativeUrl.toString());
        return converter(response);
    }

    /** See {@link TwinfinityApiClient.getInfo} */
    public async getInfo(): Promise<TwinfinityInfo> {
        return {
            name: 'Dev',
            logoImageUrl: 'http://foo.com/twinfinity/bar.png',
            backgroundImageUrl: 'http://foo.com/twinfinity/bar.png',
            psUrl: 'http://foo.com',
            version: '0.0.0.0'
        };
    }

    private makeAbsoluteUrl(url: URL | string): URL {
        this._anchorElement.href = url.toString();
        return new URL(this._anchorElement.href);
    }

    private getContainerId(url: URL): string | null {
        return url.searchParams.get('container') ?? 'root';
    }
}
