// Copyright (c) Microsoft Corporation and Henry Lucco.
// Licensed under the MIT License.

import { getUserDataDir } from "agent-dispatcher/helpers/data";
import { readFileSync } from "node:fs";
import { initRuntimeConfigFromProcessEnv } from "aiclient";
import {
    TypeAgentAPIServerConfig,
    TypeAgentAPIWebServer,
} from "./webServer.js";
import { TypeAgentAPIWebSocketServer } from "./webSocketServer.js";
import {
    // BlobServiceClient,
    // BlockBlobClient,
    ContainerClient,
    ContainerListBlobsOptions,
} from "@azure/storage-blob";
// import { DefaultAzureCredential } from "@azure/identity";
import { StopWatch } from "telemetry";
import path from "node:path";
import fs from "node:fs";
import { isDirectoryPath } from "typeagent";
import { TypeAgentStorageProvider } from "./storageProvider.js";
import { AzureStorageProvider } from "./storageProviders/azureStorageProvider.js";
import { AWSStorageProvider } from "./storageProviders/awsStorageProvider.js";
import { WebDispatcher, createWebDispatcher } from "./webDispatcher.js";
import registerDebug from "debug";

const debug = registerDebug("typeagent:webserver:api");
registerDebug.enable("typeagent:webserver:*");

export class TypeAgentServer {
    private webDispatcher: WebDispatcher | undefined;
    private webSocketServer: TypeAgentAPIWebSocketServer | undefined;
    private webServer: TypeAgentAPIWebServer | undefined;
    private fileWriteDebouncer: Map<string, number> = new Map<string, number>();
    private storageProvider: TypeAgentStorageProvider | undefined;
    private config: TypeAgentAPIServerConfig;

    constructor() {
        // Build typed runtime Config from process.env (already populated
        // by loadConfig in the entry point) so aiclient consumers can
        // use the typed accessor; legacy callers still see the same
        // values through process.env directly.
        initRuntimeConfigFromProcessEnv();

        // web server config
        this.config = JSON.parse(readFileSync("data/config.json").toString());

        debug(`Loaded web config from `);

        const storageProviderMap = {
            azure: AzureStorageProvider,
            aws: AWSStorageProvider,
        };

        // setting storage provider if "provided"
        if (this.config.blobBackupEnabled && this.config.storageProvider) {
            this.storageProvider = new storageProviderMap[
                this.config.storageProvider
            ]();
            debug(`Storage provider setup: ${this.config.storageProvider}`);
        } else {
            debug(
                `Skipping storage provider setup [backupEnabled: ${this.config.blobBackupEnabled}] [provider: ${this.config.storageProvider}]`,
            );
        }
    }

    async start() {
        // restore & enable session backup?
        if (this.config.blobBackupEnabled && this.storageProvider) {
            const sw = new StopWatch();
            await this.syncFromProvider();
            this.startLocalStorageBackup();
            sw.stop("Downloaded Session Backup");
        }

        this.webDispatcher = await createWebDispatcher();
        debug("Web Dispatcher created.");

        // web server
        this.webServer = new TypeAgentAPIWebServer(
            this.config,
            (action: any) => {
                // a client passed in an action to perform
                this.webDispatcher?.handleAction(action);
            },
        );
        this.webServer.start();
        debug("TypeAgent Web Server created.");

        // websocket server
        this.webSocketServer = new TypeAgentAPIWebSocketServer(
            this.webServer.server,
            this.webDispatcher.connect,
        );
        debug("WebSocket Server created.");
    }

    stop() {
        this.webServer?.stop();
        this.webSocketServer?.stop();
        this.webDispatcher?.close();
    }

    /**
     * Downloads from session data blob storage to the local session store
     */
    /*
    async syncBlobStorage() {
        const blobServiceClient = new BlobServiceClient(
            this.accountURL,
            new DefaultAzureCredential(),
        );

        const containerClient: ContainerClient =
            blobServiceClient.getContainerClient(this.containerName!!);

        await this.findBlobs(containerClient);
    }*/

    async syncFromProvider() {
        if (!this.storageProvider) {
            console.log("No storage provider found");
            return;
        }
        const remoteFiles = await this.storageProvider.listRemoteFiles();
        for (const remoteFile of remoteFiles) {
            try {
                console.log("Syncing file: ", remoteFile);
                const localPath = path.join(getUserDataDir(), remoteFile);
                if (!fs.existsSync(localPath)) {
                    await this.storageProvider.downloadFile(
                        remoteFile,
                        localPath,
                    );
                    console.log(`Downloaded ${remoteFile} to ${localPath}`);
                }
            } catch (e) {
                console.log("Error syncing file: ", remoteFile, e);
            }
        }
    }

    /**
     * Enumerates and downloads the blobs for the supplied container client.
     * @param containerClient The container client whose blobs we are enumerating.
     */
    async findBlobs(containerClient: ContainerClient): Promise<void> {
        const maxPageSize = 100;

        // Some options for filtering results
        const listOptions: ContainerListBlobsOptions = {
            includeMetadata: false,
            includeSnapshots: false,
            prefix: "", // Filter results by blob name prefix
        };

        for await (const response of containerClient
            .listBlobsFlat(listOptions)
            .byPage({ maxPageSize })) {
            if (response.segment.blobItems) {
                for (const blob of response.segment.blobItems) {
                    const blobClient = containerClient.getBlobClient(blob.name);
                    const filePath = path.join(getUserDataDir(), blob.name);
                    let dir = path.dirname(filePath);

                    if (!fs.existsSync(dir)) {
                        fs.mkdirSync(dir, { recursive: true });
                    }

                    // only download the file if it doesn't already exist
                    if (!fs.existsSync(filePath)) {
                        await blobClient.downloadToFile(filePath, 0);
                    }
                }
            }
        }
    }

    /**
     * Looks at the local typeagent storage and replicates any file changes to the blob storage
     */
    startLocalStorageBackup() {
        fs.watch(
            getUserDataDir(),
            { recursive: true, persistent: false },
            async (_, fileName) => {
                if (
                    fileName === undefined ||
                    fileName === null ||
                    fileName?.toLowerCase().endsWith(".lock") ||
                    isDirectoryPath(path.join(getUserDataDir(), fileName!!))
                ) {
                    console.log(`Invalid file: ${fileName}`);
                    return;
                }

                // start a debouncer for the file writes
                if (!this.fileWriteDebouncer.has(fileName!!)) {
                    this.fileWriteDebouncer.set(fileName!!, 0);
                }

                // increase refcount
                let refCount: number = this.fileWriteDebouncer.get(
                    fileName!!,
                ) as number;
                this.fileWriteDebouncer.set(fileName!!, ++refCount);

                // debounce the file after some timeout
                this.debounceFileThenUpload(fileName!!);
            },
        );
    }

    /**
     * Debounces the refcount on a file write operation and then uploads the file to blob storage
     * when the refcount is at zero
     * @param fileName The file name to debounce then upload
     */
    debounceFileThenUpload(fileName: string) {
        setTimeout(async () => {
            if (!this.fileWriteDebouncer.has(fileName)) {
                return;
            }

            // drop the file bounce counter by one
            this.fileWriteDebouncer.set(
                fileName,
                this.fileWriteDebouncer.get(fileName)!! - 1,
            );

            // if the file hasn't been touched for the given timeout we can start uploading it
            const debounceCount: number =
                this.fileWriteDebouncer.get(fileName)!!;
            if (debounceCount == 0) {
                try {
                    const localPath: string = path.join(
                        getUserDataDir(),
                        fileName,
                    );

                    if (!this.storageProvider) {
                        console.log(
                            `Failed to upload ${fileName} to provider, no storage provider found`,
                        );
                        return;
                    }
                    await this.storageProvider.uploadFile(localPath, fileName);
                } catch (e) {
                    console.log(e);
                }

                // remove the file from the debouncer
                this.fileWriteDebouncer.delete(fileName);
            } else {
                this.debounceFileThenUpload(fileName);
            }
        }, 5000);
    }
}
