import {
    db,
    newts,
    blueprints,
    Blueprint,
    Site,
    siteResources,
    roleSiteResources,
    userSiteResources,
    clientSiteResources
} from "@server/db";
import { Config, ConfigSchema } from "./types";
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { sites } from "@server/db";
import { eq, and, isNotNull } from "drizzle-orm";
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
import {
    ClientResourcesResults,
    updateClientResources
} from "./clientResources";
import { BlueprintSource } from "@server/routers/blueprints/types";
import { stringify as stringifyYaml } from "yaml";
import { generateName } from "@server/db/names";
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";

type ApplyBlueprintArgs = {
    orgId: string;
    configData: unknown;
    name?: string;
    siteId?: number;
    source?: BlueprintSource;
};

export async function applyBlueprint({
    orgId,
    configData,
    siteId,
    name,
    source = "API"
}: ApplyBlueprintArgs): Promise<Blueprint> {
    // Validate the input data
    const validationResult = ConfigSchema.safeParse(configData);
    if (!validationResult.success) {
        throw new Error(fromError(validationResult.error).toString());
    }

    const config: Config = validationResult.data;
    let blueprintSucceeded: boolean = false;
    let blueprintMessage: string;
    let error: any | null = null;

    try {
        let proxyResourcesResults: ProxyResourcesResults = [];
        let clientResourcesResults: ClientResourcesResults = [];
        await db.transaction(async (trx) => {
            proxyResourcesResults = await updateProxyResources(
                orgId,
                config,
                trx,
                siteId
            );
            clientResourcesResults = await updateClientResources(
                orgId,
                config,
                trx,
                siteId
            );

            logger.debug(
                `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
            );

            // We need to update the targets on the newts from the successfully updated information
            for (const result of proxyResourcesResults) {
                for (const target of result.targetsToUpdate) {
                    const [site] = await trx
                        .select()
                        .from(sites)
                        .innerJoin(newts, eq(sites.siteId, newts.siteId))
                        .where(
                            and(
                                eq(sites.siteId, target.siteId),
                                eq(sites.orgId, orgId),
                                eq(sites.type, "newt"),
                                isNotNull(sites.pubKey)
                            )
                        )
                        .limit(1);

                    if (site) {
                        logger.debug(
                            `Updating target ${target.targetId} on site ${site.sites.siteId}`
                        );

                        // see if you can find a matching target health check from the healthchecksToUpdate array
                        const matchingHealthcheck =
                            result.healthchecksToUpdate.find(
                                (hc) => hc.targetId === target.targetId
                            );

                        await addProxyTargets(
                            site.newt.newtId,
                            [target],
                            matchingHealthcheck ? [matchingHealthcheck] : [],
                            result.proxyResource.mode === "udp" ? "udp" : "tcp",
                            site.newt.version
                        );
                    }
                }
            }

            logger.debug(
                `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
            );

            // We need to update the targets on the newts from the successfully updated information
            for (const result of clientResourcesResults) {
                if (
                    result.oldSiteResource &&
                    JSON.stringify(result.newSites?.sort()) !==
                        JSON.stringify(result.oldSites?.sort())
                ) {
                    // query existing associations
                    const existingRoleIds = await trx
                        .select()
                        .from(roleSiteResources)
                        .where(
                            eq(
                                roleSiteResources.siteResourceId,
                                result.oldSiteResource.siteResourceId
                            )
                        )
                        .then((rows) => rows.map((row) => row.roleId));

                    const existingUserIds = await trx
                        .select()
                        .from(userSiteResources)
                        .where(
                            eq(
                                userSiteResources.siteResourceId,
                                result.oldSiteResource.siteResourceId
                            )
                        )
                        .then((rows) => rows.map((row) => row.userId));

                    const existingClientIds = await trx
                        .select()
                        .from(clientSiteResources)
                        .where(
                            eq(
                                clientSiteResources.siteResourceId,
                                result.oldSiteResource.siteResourceId
                            )
                        )
                        .then((rows) => rows.map((row) => row.clientId));

                    // delete the existing site resource
                    await trx
                        .delete(siteResources)
                        .where(
                            and(
                                eq(
                                    siteResources.siteResourceId,
                                    result.oldSiteResource.siteResourceId
                                )
                            )
                        );

                    await rebuildClientAssociationsFromSiteResource(
                        result.oldSiteResource,
                        trx
                    );

                    const [insertedSiteResource] = await trx
                        .insert(siteResources)
                        .values({
                            ...result.newSiteResource
                        })
                        .returning();

                    // wait some time to allow for messages to be handled
                    await new Promise((resolve) => setTimeout(resolve, 750));

                    //////////////////// update the associations ////////////////////

                    if (existingRoleIds.length > 0) {
                        await trx.insert(roleSiteResources).values(
                            existingRoleIds.map((roleId) => ({
                                roleId,
                                siteResourceId:
                                    insertedSiteResource!.siteResourceId
                            }))
                        );
                    }

                    if (existingUserIds.length > 0) {
                        await trx.insert(userSiteResources).values(
                            existingUserIds.map((userId) => ({
                                userId,
                                siteResourceId:
                                    insertedSiteResource!.siteResourceId
                            }))
                        );
                    }

                    if (existingClientIds.length > 0) {
                        await trx.insert(clientSiteResources).values(
                            existingClientIds.map((clientId) => ({
                                clientId,
                                siteResourceId:
                                    insertedSiteResource!.siteResourceId
                            }))
                        );
                    }

                    await rebuildClientAssociationsFromSiteResource(
                        insertedSiteResource,
                        trx
                    );
                } else {
                    let good = true;
                    for (const newSite of result.newSites) {
                        const [site] = await trx
                            .select()
                            .from(sites)
                            .innerJoin(newts, eq(sites.siteId, newts.siteId))
                            .where(
                                and(
                                    eq(sites.siteId, newSite.siteId),
                                    eq(sites.orgId, orgId),
                                    eq(sites.type, "newt"),
                                    isNotNull(sites.pubKey)
                                )
                            )
                            .limit(1);

                        if (!site) {
                            logger.debug(
                                `No newt sites found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
                            );
                            good = false;
                            break;
                        }

                        logger.debug(
                            `Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.siteId}`
                        );
                    }

                    if (!good) {
                        continue;
                    }

                    await handleMessagingForUpdatedSiteResource(
                        result.oldSiteResource,
                        result.newSiteResource,
                        result.newSites.map((site) => ({
                            siteId: site.siteId,
                            orgId: result.newSiteResource.orgId
                        })),
                        trx
                    );
                }

                // await addClientTargets(
                //     site.newt.newtId,
                //     result.resource.destination,
                //     result.resource.destinationPort,
                //     result.resource.protocol,
                //     result.resource.proxyPort
                // );
            }
        });

        blueprintSucceeded = true;
        blueprintMessage = "Blueprint applied successfully";
    } catch (err) {
        blueprintSucceeded = false;
        blueprintMessage = `Blueprint applied with errors: ${err}`;
        logger.error(blueprintMessage);
        error = err;
    }

    let blueprint: Blueprint | null = null;
    await db.transaction(async (trx) => {
        const newBlueprint = await trx
            .insert(blueprints)
            .values({
                orgId,
                name: name ?? generateName(),
                contents: stringifyYaml(configData),
                createdAt: Math.floor(Date.now() / 1000),
                succeeded: blueprintSucceeded,
                message: blueprintMessage,
                source
            })
            .returning();

        blueprint = newBlueprint[0];
    });

    if (!blueprint || (source !== "UI" && !blueprintSucceeded)) {
        //             ^^^^^^^^^^^^^^^ The UI considers a failed blueprint as a valid response
        throw error ?? "Unknown Server Error";
    }

    return blueprint;
}
