import { BimCoreApi } from './BimCoreApi';
import {
    BimContainerInfo,
    TwinfinityInfo,
    BimContainer,
    BimChangeType,
    HttpIfcResourceUrls
} from './loader/bim-api-client';
import { BimApiLoadOptions } from './BimApiLoadOptions';
import { BimTwinfinityApiClient } from './loader/client/BimTwinfinityApiClient';
import { AuthorizationHeaderProvider, Http, HttpStatusCode } from './http';
import { TwinfinityApiClient } from './loader/client/TwinfinityApiClient';
import { telemetry } from './Telemetry';
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { TwinfinityApiOptions } from './loader/client/TwinfinityApiOptions';
import type { MeshStoreLoaderOptions } from '@twinfinity/geometry';
import { wrapAuthorizationHeaderProvider } from './loader/client/AuthHelpers';
import { Discipline } from './loader/discipline';
import { Permissions } from './loader/Permission';
import { LayerDefinitions } from './loader/LayerDefinitions';
import type { TwinInput, IfcWithOptions } from './loader/bim-ifc-loader';

/**
 * Creation options for {@link BimApi}
 */
export type BimApiOptions = (
    | {
          /**
           * If `true` then {@link BimApi} will automatically redirect the user
           * to the PS login page if the user is not already authenticated.
           */
          readonly isPSAuthenticationRedirectEnabled?: false;
          session?: TwinfinityApiOptions['session'];
      }
    | {
          /**
           * If `true` then {@link BimApi} will automatically redirect the user
           * to the PS login page if the user is not already authenticated.
           */
          readonly isPSAuthenticationRedirectEnabled: true;
      }
) & {
    /** set this to a unique name for the appliation. Will allow log filtering on application name. */
    readonly applicationName?: string;
    /** Options for WASM-based geometry deflation. If provided, enables WASM loading. */
    readonly wasmLoaderOptions?: MeshStoreLoaderOptions;
};

/**
 * BimApi is the main API class. This a subclass of {@link BimCoreApi} that is extended
 * with information only available from the Twinfinity API backend.
 */
export class BimApi extends BimCoreApi {
    private _info?: TwinfinityInfo;
    private _containerInfo?: BimContainerInfo;
    private readonly _bimApiOptions: Omit<BimApiOptions, 'session'>;
    private _twinfinitySession?: AuthorizationHeaderProvider;

    /**
     * Creates a new instance of {@link BimApi}.
     * @param _htmlElementOrId ID of the HTML element where the viewer should be created.
     * @param absoluteContainerUrlOrApiClient Absolute URL to the container.
     * @param options Creation options.
     * @example
     * ```typescript
     * // Create an API instance.
     * const api = await BimApi.create('viewer', 'project-url'); // Open project directly.
     * const api = await BimApi.create('viewer', new BimTwinfinityApiClient(url)); // Create API. If a valid container url is given, the API automatically switches to it, otherwise calls api.setContainer() afterwards.
     * ```
     */
    public static async create(
        _htmlElementOrId: string | HTMLElement,
        absoluteContainerUrlOrApiClient: URL | string | TwinfinityApiClient,
        options?: BimApiOptions
    ): Promise<BimApi> {
        if (options?.applicationName) {
            telemetry.role = options.applicationName;
        }
        if (BimApi.isTwinfinityApiClient(absoluteContainerUrlOrApiClient)) {
            return BimCoreApi.createApi(
                _htmlElementOrId,
                (canvas) => new BimApi(canvas, absoluteContainerUrlOrApiClient, options)
            );
        }

        // absoluteContainerUrlOrApiClient was either a URL or a string representing a URL
        const absoluteUrl = new URL(decodeURI(absoluteContainerUrlOrApiClient.toString()).toLowerCase());

        const session = options && 'session' in options ? options.session : undefined;
        const apiClient = new BimTwinfinityApiClient(absoluteUrl, {
            session,
            applicationName: options?.applicationName
        });
        const api = await BimCoreApi.createApi(_htmlElementOrId, (canvas) => new BimApi(canvas, apiClient, options));

        try {
            await api.setContainer(absoluteUrl);
        } catch (err) {
            // Means that the passed url did not point to a valid container. In that case we just return the API
            // the user will have to call api.setContainer() later with a valid project. This can happen if API is created like this
            // created('canvasId', "https://bim.demo.projektstruktur.se") which is a valid use case.
            telemetry.trackTrace({
                message: `Failed to setContainer(${absoluteUrl.href}).`,
                severityLevel: SeverityLevel.Error
            });
        }
        return api;
    }

    /** Constructor.
     * @param canvas Canvas element where 3D content will be rendered.
     * @param _apiClient Methods for communicating with the backend.
     */
    public constructor(
        canvas: HTMLCanvasElement,
        private readonly _apiClient: TwinfinityApiClient,
        bimApiOptions?: BimApiOptions
    ) {
        super(canvas, _apiClient, {
            wasmLoaderOptions: bimApiOptions?.wasmLoaderOptions,
            twinInputResolver: (input) => BimApi.resolveTwinInput(_apiClient, input)
        });
        const givenOptions = bimApiOptions ?? { isPSAuthenticationRedirectEnabled: false };
        this._bimApiOptions = {
            isPSAuthenticationRedirectEnabled: givenOptions.isPSAuthenticationRedirectEnabled
        };
        if (bimApiOptions?.isPSAuthenticationRedirectEnabled !== true) {
            this._twinfinitySession = bimApiOptions?.session;
        }
    }

    /** General Twinfinity information such as customer portal name, Twinfinity API version etc. */
    public info(): TwinfinityInfo {
        return this._info!;
    }

    /** Direct access to the methods for communicating the backend. */
    public get backend(): TwinfinityApiClient {
        return this._apiClient;
    }

    /**
     * Gives information on currently selected container such as title, language
     * and also details of the current user.
     */
    public get currentContainerInfo(): BimContainerInfo {
        return this._containerInfo!;
    }

    /**
     * Sets the container of the current BimApi instance.
     * @param containerOrUrl Container object or absolute URL Example: https://bim.foo.com/sites/portal/projects/\{project name\}
     * @param options Loading options
     */
    public async setContainer(containerOrUrl: BimContainer | URL, options?: BimApiLoadOptions): Promise<void> {
        const url = 'url' in containerOrUrl ? new URL(containerOrUrl.url) : containerOrUrl;
        const containerInfo = await this.backend.getContainerInfo(url);
        if (!containerInfo.ok) {
            throw new Error(`${url.href} does not exist.`);
        }
        this._containerInfo = await containerInfo.value;
        return await super.setContainer(containerOrUrl, options);
    }

    private static async resolveTwinInput(apiClient: TwinfinityApiClient, input: TwinInput): Promise<IfcWithOptions> {
        const { twinId, version } = input;
        const twinClient = apiClient.twins;

        const twin = await twinClient.getTwin(version !== undefined ? { id: twinId, version } : twinId);

        const twinContent =
            version !== undefined
                ? await twinClient.getTwinVersionContent(twinId, version)
                : await twinClient.getTwinContent(twinId);

        if (!twinContent.content.find((c) => c.format === 't8cm')) {
            throw new Error(`Twin ${twinId} does not have a client model (t8cm)`);
        }

        const resolvedVersion = twinContent.twinVersion;
        const baseUrl = BimApi.getBaseUrl(apiClient).toString().replace(/\/$/, '');
        const contentsUrl = `${baseUrl}/_ps/twins/${twinId}/versions/${resolvedVersion}/contents`;
        const contentByFormat = new Map(
            twinContent.content.map((c) => [c.format, new URL(`${contentsUrl}/${c.id}/content`)])
        );

        const resourceUrl: HttpIfcResourceUrls = {
            t8cm: contentByFormat.get('t8cm'),
            t8gd: contentByFormat.get('t8gd'),
            t8gi: contentByFormat.get('t8gi')
        };

        return {
            type: BimChangeType.Ifc,
            ownerSystem: 'twinfinity',
            id: twin.id,
            version: resolvedVersion,
            name: twin.name,
            url: `${baseUrl}/_ps/twins/${twin.id}`,
            path: `/_ps/twins/${twin.id}`,
            containerId: twin.relatedContainerIds?.[0] ?? '',
            blobId: `twin_${twin.id}_v${resolvedVersion}`,
            format: 'ifc',
            discipline: Discipline.getOrAdd('NA'),
            productCount: -1,
            etag: twin.etag ?? '',
            metadata: {
                ifc: {
                    hasParseError: false,
                    status: 'Processed',
                    url: {
                        t8gd: resourceUrl.t8gd?.toString()
                    }
                },
                serverRelativeUrl: `/_ps/twins/${twin.id}`
            },
            floors: [],
            classes: [],
            permissionsString: '',
            permissions: new Permissions('view'),
            apiUrl: `${baseUrl}/_ps/api/`,
            resourceUrl: resourceUrl,
            availableLayerDefinitions: new LayerDefinitions(),
            transform: input.transform
        } satisfies IfcWithOptions;
    }

    private static getBaseUrl(apiClient: TwinfinityApiClient): URL {
        if ('baseUrl' in apiClient && apiClient.baseUrl instanceof URL) {
            return apiClient.baseUrl;
        }
        return new URL(`https://${apiClient.id}`);
    }

    protected async initialize(): Promise<void> {
        await super.initialize();

        if (this._twinfinitySession != null) {
            const session = this._twinfinitySession;

            Http.setAuthorizationHeaderProvider(wrapAuthorizationHeaderProvider(session));
            delete this._twinfinitySession;
        }

        const rootContainerInfoResponse = await this.backend.getContainerInfo(); // No initial url get top most container info
        if (
            this._bimApiOptions?.isPSAuthenticationRedirectEnabled &&
            rootContainerInfoResponse.status === HttpStatusCode.Unauthorized
        ) {
            telemetry.trackTrace({
                severityLevel: SeverityLevel.Warning,
                message:
                    'Authentication by third party cookies has been deprecated. See https://help.twinfinity.com/space/HCFD/125370380/KB%3A+Changes+in+Authentication+Method for further information.'
            });

            // Redirect to projektstruktur login page and force user to login. Projekstruktur will
            // then redirect us back to the page where this code is hosted. Hence we will start executing
            // the this.initialize() method again. However this time container info will report
            // that we are logged in.
            const returnUrl = encodeURIComponent(window.parent.location.href);
            const psUrl = BimApi.getPsUrl(rootContainerInfoResponse.url);
            window.parent.location.href = `${psUrl.protocol}//${psUrl.hostname}/sites/portal/_layouts/PS_Core/Twinfinity/Login.aspx?TFUrl=${returnUrl}`;
        } else if (rootContainerInfoResponse.status === HttpStatusCode.NotFound) {
            throw new Error(`Backend API does not exist at ${rootContainerInfoResponse.url}`);
        } else if (!rootContainerInfoResponse.ok) {
            throw new Error(
                `Failed to connect to ${rootContainerInfoResponse.url}. Http status ${rootContainerInfoResponse.status}, ${rootContainerInfoResponse.statusText}`
            );
        }

        this._containerInfo = await rootContainerInfoResponse.value;
        telemetry.setAuthenticatedUserContext(this._containerInfo.user.name, telemetry.accountId);
        // Call system endpoints to get system info
        this._info = await this.backend.getInfo();
    }

    private static getPsUrl(url: string): URL {
        const psUrl = new URL(url);
        psUrl.hostname = psUrl.hostname.replace(/^bim\./i, '');
        return psUrl;
    }

    private static isTwinfinityApiClient(arg: unknown): arg is TwinfinityApiClient {
        return typeof (arg as TwinfinityApiClient).getContainers === 'function';
    }
}
