import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
import type { Project } from "~/models/project.server";
import type { User } from "~/models/user.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
import type { EnvironmentVariableUpdater } from "~/v3/environmentVariables/repository";
import { SyncEnvVarsMapping, EnvSlug } from "~/v3/vercel/vercelProjectIntegrationSchema";
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
import { loadEnvironmentVariablesEnvironments } from "./environmentVariablesEnvironments.server";

type Result = Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>;
export type EnvironmentVariableWithSetValues = Result["environmentVariables"][number];

export class EnvironmentVariablesPresenter {
  #prismaClient: PrismaClient;
  #replicaClient: PrismaReplicaClient;

  constructor(prismaClient: PrismaClient = prisma, replicaClient: PrismaReplicaClient = $replica) {
    this.#prismaClient = prismaClient;
    this.#replicaClient = replicaClient;
  }

  public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) {
    const project = await this.#replicaClient.project.findFirst({
      select: {
        id: true,
      },
      where: {
        slug: projectSlug,
        organization: {
          members: {
            some: {
              userId,
            },
          },
        },
      },
    });

    if (!project) {
      throw new Error("Project not found");
    }

    const { environments: sortedEnvironments, hasStaging } =
      await loadEnvironmentVariablesEnvironments(
        this.#replicaClient,
        { userId, projectId: project.id },
        { skipProjectAccessCheck: true }
      );

    // Only load values for the environments we display. Projects can accumulate
    // values in archived branch environments, which would otherwise all be loaded here.
    const environmentIds = sortedEnvironments.map((env) => env.id);

    const environmentVariables = await this.#replicaClient.environmentVariable.findMany({
      select: {
        id: true,
        key: true,
        values: {
          select: {
            id: true,
            environmentId: true,
            version: true,
            lastUpdatedBy: true,
            updatedAt: true,
            valueReference: {
              select: {
                key: true,
              },
            },
            isSecret: true,
          },
          where: {
            environmentId: {
              in: environmentIds,
            },
          },
        },
      },
      where: {
        projectId: project.id,
      },
    });

    const userIds = new Set(
      environmentVariables
        .flatMap((envVar) => envVar.values)
        .map((value) => value.lastUpdatedBy)
        .filter(
          (lastUpdatedBy): lastUpdatedBy is { type: "user"; userId: string } =>
            lastUpdatedBy !== null &&
            typeof lastUpdatedBy === "object" &&
            "type" in lastUpdatedBy &&
            lastUpdatedBy.type === "user" &&
            "userId" in lastUpdatedBy &&
            typeof lastUpdatedBy.userId === "string"
        )
        .map((lastUpdatedBy) => lastUpdatedBy.userId)
    );

    const users =
      userIds.size > 0
        ? await this.#replicaClient.user.findMany({
            where: {
              id: {
                in: Array.from(userIds),
              },
            },
            select: {
              id: true,
              name: true,
              displayName: true,
              avatarUrl: true,
            },
          })
        : [];

    const usersRecord: Record<
      string,
      { id: string; name: string | null; displayName: string | null; avatarUrl: string | null }
    > = Object.fromEntries(users.map((u) => [u.id, u]));

    const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient);

    const nonSecretItems: Array<{ environmentId: string; key: string }> = [];
    for (const environmentVariable of environmentVariables) {
      for (const env of sortedEnvironments) {
        const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id);
        if (valueRecord && !valueRecord.isSecret) {
          nonSecretItems.push({ environmentId: env.id, key: environmentVariable.key });
        }
      }
    }

    const variableValuesByEnvAndKey = await repository.getVariableValuesForKeys(
      project.id,
      nonSecretItems
    );

    // Get Vercel integration data if it exists
    const vercelService = new VercelIntegrationService(this.#prismaClient);
    const vercelIntegration = await vercelService.getVercelProjectIntegration(project.id);

    let vercelSyncEnvVarsMapping: SyncEnvVarsMapping = {};
    let vercelPullEnvVarsBeforeBuild: EnvSlug[] | null = null;

    if (vercelIntegration) {
      vercelSyncEnvVarsMapping = vercelIntegration.parsedIntegrationData.syncEnvVarsMapping;
      vercelPullEnvVarsBeforeBuild =
        vercelIntegration.parsedIntegrationData.config.pullEnvVarsBeforeBuild ?? null;
    }

    return {
      environmentVariables: environmentVariables
        .flatMap((environmentVariable) => {
          return sortedEnvironments.flatMap((env) => {
            const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id);
            const isSecret = valueRecord?.isSecret ?? false;

            if (!valueRecord) {
              return [];
            }

            const val = isSecret
              ? undefined
              : variableValuesByEnvAndKey.get(`${env.id}:${environmentVariable.key}`);

            if (!isSecret && val === undefined) {
              return [];
            }

            const lastUpdatedBy = valueRecord.lastUpdatedBy as EnvironmentVariableUpdater | null;

            const updatedByUser =
              lastUpdatedBy?.type === "user"
                ? (() => {
                    const user = usersRecord[lastUpdatedBy.userId];
                    return user
                      ? {
                          id: user.id,
                          name: user.displayName || user.name || "Unknown",
                          avatarUrl: user.avatarUrl,
                        }
                      : null;
                  })()
                : null;

            return [
              {
                id: environmentVariable.id,
                key: environmentVariable.key,
                environment: { type: env.type, id: env.id, branchName: env.branchName },
                value: isSecret ? "" : val!,
                isSecret,
                version: valueRecord.version,
                lastUpdatedBy,
                updatedByUser,
                updatedAt: valueRecord.updatedAt,
              },
            ];
          });
        })
        .sort((a, b) => a.key.localeCompare(b.key)),
      environments: sortedEnvironments,
      hasStaging,
      // Vercel integration data
      vercelIntegration: vercelIntegration
        ? {
            enabled: true,
            pullEnvVarsBeforeBuild: vercelPullEnvVarsBeforeBuild,
            syncEnvVarsMapping: vercelSyncEnvVarsMapping,
          }
        : null,
    };
  }
}
