import type {
  PrismaClient,
  OrganizationProjectIntegration,
  OrganizationIntegration,
  SecretReference,
} from "@trigger.dev/database";
import { ResultAsync } from "neverthrow";
import { prisma, $transaction } from "~/db.server";
import { logger } from "~/services/logger.server";
import { VercelIntegrationRepository } from "~/models/vercelIntegration.server";
import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server";
import type {
  VercelProjectIntegrationData,
  VercelIntegrationConfig,
  SyncEnvVarsMapping,
  TriggerEnvironmentType,
  EnvSlug,
} from "~/v3/vercel/vercelProjectIntegrationSchema";
import {
  VercelProjectIntegrationDataSchema,
  envTypeToSlug,
  createDefaultVercelIntegrationData,
} from "~/v3/vercel/vercelProjectIntegrationSchema";

export type VercelProjectIntegrationWithParsedData = OrganizationProjectIntegration & {
  parsedIntegrationData: VercelProjectIntegrationData;
};

export type VercelProjectIntegrationWithData = VercelProjectIntegrationWithParsedData & {
  organizationIntegration: OrganizationIntegration;
};

export type VercelProjectIntegrationWithProject = VercelProjectIntegrationWithData & {
  project: {
    id: string;
    name: string;
    slug: string;
  };
};

export class VercelIntegrationService {
  #prismaClient: PrismaClient;

  constructor(prismaClient: PrismaClient = prisma) {
    this.#prismaClient = prismaClient;
  }

  async getVercelProjectIntegration(
    projectId: string
  ): Promise<VercelProjectIntegrationWithData | null> {
    const integration = await this.#prismaClient.organizationProjectIntegration.findFirst({
      where: {
        projectId,
        deletedAt: null,
        organizationIntegration: {
          service: "VERCEL",
          deletedAt: null,
        },
      },
      include: {
        organizationIntegration: true,
      },
    });

    if (!integration) {
      return null;
    }

    const parsedData = VercelProjectIntegrationDataSchema.safeParse(integration.integrationData);

    if (!parsedData.success) {
      logger.error("Failed to parse Vercel integration data", {
        projectId,
        integrationId: integration.id,
        error: parsedData.error,
      });
      return null;
    }

    return {
      ...integration,
      parsedIntegrationData: parsedData.data,
    };
  }

  async getConnectedVercelProjects(
    organizationId: string
  ): Promise<VercelProjectIntegrationWithProject[]> {
    const integrations = await this.#prismaClient.organizationProjectIntegration.findMany({
      where: {
        deletedAt: null,
        organizationIntegration: {
          organizationId,
          service: "VERCEL",
          deletedAt: null,
        },
      },
      include: {
        organizationIntegration: true,
        project: {
          select: {
            id: true,
            name: true,
            slug: true,
          },
        },
      },
    });

    return integrations
      .map((integration) => {
        const parsedData = VercelProjectIntegrationDataSchema.safeParse(
          integration.integrationData
        );
        if (!parsedData.success) {
          logger.error("Failed to parse Vercel integration data", {
            integrationId: integration.id,
            error: parsedData.error,
          });
          return null;
        }

        return {
          ...integration,
          parsedIntegrationData: parsedData.data,
        };
      })
      .filter((i): i is VercelProjectIntegrationWithProject => i !== null);
  }

  async createVercelProjectIntegration(params: {
    organizationIntegrationId: string;
    projectId: string;
    vercelProjectId: string;
    vercelProjectName: string;
    vercelTeamId: string | null;
    vercelTeamSlug?: string;
    installedByUserId?: string;
  }): Promise<OrganizationProjectIntegration> {
    const integrationData = createDefaultVercelIntegrationData(
      params.vercelProjectId,
      params.vercelProjectName,
      params.vercelTeamId,
      params.vercelTeamSlug
    );

    return this.#prismaClient.organizationProjectIntegration.create({
      data: {
        organizationIntegrationId: params.organizationIntegrationId,
        projectId: params.projectId,
        externalEntityId: params.vercelProjectId,
        integrationData: integrationData,
        installedBy: params.installedByUserId,
      },
    });
  }

  async selectVercelProject(params: {
    organizationId: string;
    projectId: string;
    vercelProjectId: string;
    vercelProjectName: string;
    userId: string;
  }): Promise<{
    integration: OrganizationProjectIntegration;
    syncResult: { success: boolean; errors: string[] };
  }> {
    const orgIntegration = await VercelIntegrationRepository.findVercelOrgIntegrationByOrganization(
      params.organizationId
    );

    if (!orgIntegration) {
      throw new Error("No Vercel organization integration found");
    }

    const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);

    const vercelTeamSlug = await VercelIntegrationRepository.getVercelClient(orgIntegration)
      .andThen((client) => VercelIntegrationRepository.getTeamSlug(client, teamId))
      .match(
        (slug) => slug,
        () => undefined
      );

    // Use a serializable transaction to prevent duplicate project integrations
    // from concurrent selectVercelProject calls (read-then-write race condition).
    const txResult = await $transaction(
      this.#prismaClient,
      "selectVercelProject",
      async (tx) => {
        const existing = await tx.organizationProjectIntegration.findFirst({
          where: {
            projectId: params.projectId,
            deletedAt: null,
            organizationIntegration: {
              service: "VERCEL",
              deletedAt: null,
            },
          },
          include: {
            organizationIntegration: true,
          },
        });

        if (existing) {
          const parsedData = VercelProjectIntegrationDataSchema.safeParse(existing.integrationData);

          const updated = await tx.organizationProjectIntegration.update({
            where: { id: existing.id },
            data: {
              externalEntityId: params.vercelProjectId,
              integrationData: {
                ...(parsedData.success ? parsedData.data : {}),
                vercelProjectId: params.vercelProjectId,
                vercelProjectName: params.vercelProjectName,
                vercelTeamId: teamId,
                vercelTeamSlug,
              },
            },
          });

          return {
            integration: updated,
            wasCreated: false,
            vercelStagingEnvironment: parsedData.success
              ? parsedData.data.config.vercelStagingEnvironment
              : null,
          };
        }

        const integrationData = createDefaultVercelIntegrationData(
          params.vercelProjectId,
          params.vercelProjectName,
          teamId,
          vercelTeamSlug
        );

        const created = await tx.organizationProjectIntegration.create({
          data: {
            organizationIntegrationId: orgIntegration.id,
            projectId: params.projectId,
            externalEntityId: params.vercelProjectId,
            integrationData: integrationData,
            installedBy: params.userId,
          },
        });

        return {
          integration: created,
          wasCreated: true,
          vercelStagingEnvironment: null,
        };
      },
      { isolationLevel: "Serializable" }
    );

    if (!txResult) {
      throw new Error("Failed to select Vercel project: transaction returned undefined");
    }

    const { integration, wasCreated, vercelStagingEnvironment } = txResult;

    const syncResultAsync = await VercelIntegrationRepository.syncApiKeysToVercel({
      projectId: params.projectId,
      vercelProjectId: params.vercelProjectId,
      teamId,
      vercelStagingEnvironment,
      orgIntegration,
    });
    const syncResult = syncResultAsync.isOk()
      ? { success: syncResultAsync.value.errors.length === 0, errors: syncResultAsync.value.errors }
      : { success: false, errors: [syncResultAsync.error.message] };

    if (wasCreated) {
      const disableResult = await VercelIntegrationRepository.getVercelClient(
        orgIntegration
      ).andThen((client) =>
        VercelIntegrationRepository.disableAutoAssignCustomDomains(
          client,
          params.vercelProjectId,
          teamId
        )
      );

      if (disableResult.isErr()) {
        logger.warn("Failed to disable autoAssignCustomDomains during project selection", {
          projectId: params.projectId,
          vercelProjectId: params.vercelProjectId,
          error: disableResult.error.message,
        });
      }

      logger.info("Vercel project selected and API keys synced", {
        projectId: params.projectId,
        vercelProjectId: params.vercelProjectId,
        vercelProjectName: params.vercelProjectName,
        syncSuccess: syncResult.success,
        syncErrors: syncResult.errors,
      });
    }

    return { integration, syncResult };
  }

  async updateVercelIntegrationConfig(
    projectId: string,
    configUpdates: Partial<VercelIntegrationConfig>
  ): Promise<VercelProjectIntegrationWithParsedData | null> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return null;
    }

    const updatedConfig = {
      ...existing.parsedIntegrationData.config,
      ...configUpdates,
    };

    const updatedData: VercelProjectIntegrationData = {
      ...existing.parsedIntegrationData,
      config: updatedConfig,
    };

    const updated = await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        integrationData: updatedData,
      },
    });

    if (!updatedConfig.atomicBuilds?.includes("prod")) {
      return { ...updated, parsedIntegrationData: updatedData };
    }

    const orgIntegration =
      await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId);

    if (orgIntegration) {
      await this.#syncTriggerVersionToVercelProduction(
        projectId,
        updatedConfig.atomicBuilds,
        orgIntegration
      );
    }

    return {
      ...updated,
      parsedIntegrationData: updatedData,
    };
  }

  async syncStagingKeyForCustomEnvironment(
    projectId: string,
    previousCustomEnvironmentId?: string | null,
    newCustomEnvironmentId?: string | null
  ) {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return;
    }

    const orgIntegration =
      await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId);
    if (!orgIntegration) {
      return;
    }

    const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
    const vercelProjectId = existing.parsedIntegrationData.vercelProjectId;

    // Remove the key from the old custom environment (if it changed or was removed)
    if (previousCustomEnvironmentId && previousCustomEnvironmentId !== newCustomEnvironmentId) {
      const removeResult = await VercelIntegrationRepository.removeEnvVarForCustomEnvironment({
        orgIntegration,
        vercelProjectId,
        teamId,
        key: "TRIGGER_SECRET_KEY",
        customEnvironmentId: previousCustomEnvironmentId,
      });

      if (removeResult.isErr()) {
        logger.error(
          "Failed to remove staging TRIGGER_SECRET_KEY from previous custom environment",
          {
            projectId,
            previousCustomEnvironmentId,
            error: removeResult.error.message,
          }
        );
      }
    }

    // Create/update the key for the new custom environment
    if (newCustomEnvironmentId) {
      const stagingEnv = await this.#prismaClient.runtimeEnvironment.findFirst({
        where: {
          projectId,
          type: "STAGING",
        },
        select: {
          apiKey: true,
        },
      });

      if (!stagingEnv) {
        logger.warn("No STAGING runtime environment found for project", { projectId });
        return;
      }

      const upsertResult = await VercelIntegrationRepository.upsertEnvVarForCustomEnvironment({
        orgIntegration,
        vercelProjectId,
        teamId,
        key: "TRIGGER_SECRET_KEY",
        value: stagingEnv.apiKey,
        customEnvironmentId: newCustomEnvironmentId,
        type: "sensitive",
      });

      if (upsertResult.isErr()) {
        logger.warn("Failed to sync staging TRIGGER_SECRET_KEY to custom environment", {
          projectId,
          newCustomEnvironmentId,
          error: upsertResult.error.message,
        });
      }
    }
  }

  async updateSyncEnvVarsMapping(
    projectId: string,
    syncEnvVarsMapping: SyncEnvVarsMapping
  ): Promise<VercelProjectIntegrationWithParsedData | null> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return null;
    }

    const updatedData: VercelProjectIntegrationData = {
      ...existing.parsedIntegrationData,
      syncEnvVarsMapping,
    };

    const updated = await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        integrationData: updatedData,
      },
    });

    return {
      ...updated,
      parsedIntegrationData: updatedData,
    };
  }

  async updateSyncEnvVarForEnvironment(
    projectId: string,
    envVarKey: string,
    environmentType: TriggerEnvironmentType,
    syncEnabled: boolean
  ): Promise<VercelProjectIntegrationWithParsedData | null> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return null;
    }

    const currentMapping = existing.parsedIntegrationData.syncEnvVarsMapping || {};
    const envSlug = envTypeToSlug(environmentType);

    const currentEnvSettings = currentMapping[envSlug] || {};

    const updatedMapping: SyncEnvVarsMapping = {
      ...currentMapping,
      [envSlug]: {
        ...currentEnvSettings,
        [envVarKey]: syncEnabled,
      },
    };

    const updatedData: VercelProjectIntegrationData = {
      ...existing.parsedIntegrationData,
      syncEnvVarsMapping: updatedMapping,
    };

    const updated = await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        integrationData: updatedData,
      },
    });

    return {
      ...updated,
      parsedIntegrationData: updatedData,
    };
  }

  async removeSyncEnvVarForEnvironment(
    projectId: string,
    envVarKey: string,
    environmentType: TriggerEnvironmentType
  ): Promise<void> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) return;

    const currentMapping = existing.parsedIntegrationData.syncEnvVarsMapping || {};
    const envSlug = envTypeToSlug(environmentType);
    const currentEnvSettings = currentMapping[envSlug];
    if (!currentEnvSettings || !(envVarKey in currentEnvSettings)) return;

    const { [envVarKey]: _, ...rest } = currentEnvSettings;
    const updatedMapping = { ...currentMapping, [envSlug]: rest };

    await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        integrationData: {
          ...existing.parsedIntegrationData,
          syncEnvVarsMapping: updatedMapping,
        },
      },
    });
  }

  async completeOnboarding(
    projectId: string,
    params: {
      vercelStagingEnvironment?: { environmentId: string; displayName: string } | null;
      pullEnvVarsBeforeBuild?: EnvSlug[] | null;
      atomicBuilds?: EnvSlug[] | null;
      discoverEnvVars?: EnvSlug[] | null;
      syncEnvVarsMapping?: SyncEnvVarsMapping;
      origin?: "marketplace" | "dashboard";
    }
  ): Promise<VercelProjectIntegrationWithParsedData | null> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return null;
    }

    const syncEnvVarsMapping = params.syncEnvVarsMapping ?? {
      dev: {},
      stg: {},
      prod: {},
      preview: {},
    };
    const updatedData: VercelProjectIntegrationData = {
      ...existing.parsedIntegrationData,
      config: {
        ...existing.parsedIntegrationData.config,
        pullEnvVarsBeforeBuild: params.pullEnvVarsBeforeBuild ?? null,
        atomicBuilds: params.atomicBuilds ?? null,
        discoverEnvVars: params.discoverEnvVars ?? null,
        vercelStagingEnvironment: params.vercelStagingEnvironment ?? null,
      },
      //This is intentionally not updated here, in case of resetting the onboarding it should not override the existing mapping with an empty one
      syncEnvVarsMapping: existing.parsedIntegrationData.syncEnvVarsMapping,
      onboardingCompleted: true,
      onboardingOrigin: params.origin ?? existing.parsedIntegrationData.onboardingOrigin,
    };

    const updated = await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        integrationData: updatedData,
      },
    });

    const orgIntegration =
      await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId);

    if (orgIntegration) {
      const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);

      const pullResult = await VercelIntegrationRepository.pullEnvVarsFromVercel({
        projectId,
        vercelProjectId: updatedData.vercelProjectId,
        teamId,
        vercelStagingEnvironment: params.vercelStagingEnvironment,
        syncEnvVarsMapping,
        orgIntegration,
      });

      if (pullResult.isErr()) {
        logger.error("Failed to pull env vars from Vercel during onboarding", {
          projectId,
          error: pullResult.error.message,
        });
      } else if (pullResult.value.errors.length > 0) {
        logger.warn("Errors pulling env vars from Vercel during onboarding", {
          projectId,
          errors: pullResult.value.errors,
        });
      }

      await this.#syncTriggerVersionToVercelProduction(
        projectId,
        updatedData.config.atomicBuilds,
        orgIntegration
      );
    }

    return {
      ...updated,
      parsedIntegrationData: updatedData,
    };
  }

  async #syncTriggerVersionToVercelProduction(
    projectId: string,
    atomicBuilds: string[] | null | undefined,
    orgIntegration: OrganizationIntegration & { tokenReference: SecretReference }
  ): Promise<void> {
    if (!atomicBuilds?.includes("prod")) {
      return;
    }

    const prodEnvironment = await this.#prismaClient.runtimeEnvironment.findFirst({
      where: {
        projectId,
        type: "PRODUCTION",
      },
      select: {
        id: true,
      },
    });

    if (!prodEnvironment) {
      return;
    }

    const currentDeployment = await findCurrentWorkerDeployment({
      environmentId: prodEnvironment.id,
    });

    if (!currentDeployment?.version) {
      return;
    }

    const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
    if (clientResult.isErr()) {
      logger.error("Failed to get Vercel client for TRIGGER_VERSION sync", {
        projectId,
        error: clientResult.error.message,
      });
      return;
    }
    const client = clientResult.value;
    const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);

    // Get the Vercel project ID from the project integration
    const projectIntegration = await this.#prismaClient.organizationProjectIntegration.findFirst({
      where: {
        projectId,
        organizationIntegrationId: orgIntegration.id,
        deletedAt: null,
      },
      select: {
        externalEntityId: true,
      },
    });

    if (!projectIntegration) {
      return;
    }

    const vercelProjectId = projectIntegration.externalEntityId;

    // Check if TRIGGER_VERSION already exists targeting production
    const envVarsResult = await VercelIntegrationRepository.getVercelEnvironmentVariables(
      client,
      vercelProjectId,
      teamId
    );

    if (envVarsResult.isErr()) {
      logger.warn("Failed to fetch Vercel env vars for TRIGGER_VERSION sync", {
        projectId,
        vercelProjectId,
        error: envVarsResult.error.message,
      });
      return;
    }

    const existingTriggerVersion = envVarsResult.value.find(
      (env) => env.key === "TRIGGER_VERSION" && env.target.includes("production")
    );

    if (existingTriggerVersion) {
      return;
    }

    // Push TRIGGER_VERSION to Vercel production
    const createResult = await ResultAsync.fromPromise(
      client.projects.createProjectEnv({
        idOrName: vercelProjectId,
        ...(teamId && { teamId }),
        upsert: "true",
        requestBody: {
          key: "TRIGGER_VERSION",
          value: currentDeployment.version,
          target: ["production"] as any,
          type: "encrypted",
        },
      }),
      (error) => error
    );

    if (createResult.isErr()) {
      logger.error("Failed to sync TRIGGER_VERSION to Vercel production", {
        projectId,
        vercelProjectId,
        error:
          createResult.error instanceof Error
            ? createResult.error.message
            : String(createResult.error),
      });
      return;
    }

    logger.info("Synced TRIGGER_VERSION to Vercel production", {
      projectId,
      vercelProjectId,
      version: currentDeployment.version,
    });
  }

  /**
   * Returns true when TRIGGER_VERSION is no longer pinned on Vercel production after the call
   * (either we cleared it or it wasn't set to begin with). Returns false when we failed to
   * verify or perform the delete — callers should surface that to the user so they can clear
   * it manually.
   */
  async clearTriggerVersionFromVercelProduction(projectId: string): Promise<boolean> {
    const orgIntegration =
      await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId);
    if (!orgIntegration) {
      return false;
    }

    const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
    if (clientResult.isErr()) {
      logger.error("Failed to get Vercel client for TRIGGER_VERSION clear", {
        projectId,
        error: clientResult.error.message,
      });
      return false;
    }
    const client = clientResult.value;
    const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);

    const projectIntegration = await this.#prismaClient.organizationProjectIntegration.findFirst({
      where: {
        projectId,
        organizationIntegrationId: orgIntegration.id,
        deletedAt: null,
      },
      select: {
        externalEntityId: true,
      },
    });

    if (!projectIntegration) {
      return false;
    }

    const vercelProjectId = projectIntegration.externalEntityId;

    const envVarsResult = await VercelIntegrationRepository.getVercelEnvironmentVariables(
      client,
      vercelProjectId,
      teamId
    );

    if (envVarsResult.isErr()) {
      logger.warn("Failed to fetch Vercel env vars for TRIGGER_VERSION clear", {
        projectId,
        vercelProjectId,
        error: envVarsResult.error.message,
      });
      return false;
    }

    const existingTriggerVersion = envVarsResult.value.find(
      (env) => env.key === "TRIGGER_VERSION" && env.target.includes("production")
    );

    if (!existingTriggerVersion) {
      logger.info("TRIGGER_VERSION not present on Vercel production — nothing to clear", {
        projectId,
        vercelProjectId,
      });
      return true;
    }

    const removeResult = await ResultAsync.fromPromise(
      client.projects.batchRemoveProjectEnv({
        idOrName: vercelProjectId,
        ...(teamId && { teamId }),
        requestBody: { ids: [existingTriggerVersion.id] },
      }),
      (error) => error
    );

    if (removeResult.isErr()) {
      logger.error("Failed to clear TRIGGER_VERSION from Vercel production", {
        projectId,
        vercelProjectId,
        error:
          removeResult.error instanceof Error
            ? removeResult.error.message
            : String(removeResult.error),
      });
      return false;
    }

    logger.info("Cleared TRIGGER_VERSION from Vercel production", {
      projectId,
      vercelProjectId,
    });
    return true;
  }

  async disconnectVercelProject(projectId: string): Promise<boolean> {
    const existing = await this.getVercelProjectIntegration(projectId);
    if (!existing) {
      return false;
    }

    await this.#prismaClient.organizationProjectIntegration.update({
      where: { id: existing.id },
      data: {
        deletedAt: new Date(),
      },
    });

    return true;
  }
}
