import { type PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { DeleteProjectService } from "~/services/deleteProject.server";
import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github";
import { checkGitHubBranchExists } from "~/services/gitHub.server";
import { errAsync, fromPromise, okAsync, ResultAsync } from "neverthrow";
import { type BuildSettings } from "~/v3/buildSettings";

export class ProjectSettingsService {
  #prismaClient: PrismaClient;

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

  renameProject(projectId: string, newName: string) {
    return fromPromise(
      this.#prismaClient.project.update({
        where: {
          id: projectId,
        },
        data: {
          name: newName,
        },
      }),
      (error) => ({
        type: "other" as const,
        cause: error,
      })
    );
  }

  deleteProject(projectSlug: string, userId: string) {
    const deleteProjectService = new DeleteProjectService(this.#prismaClient);

    return fromPromise(deleteProjectService.call({ projectSlug, userId }), (error) => ({
      type: "other" as const,
      cause: error,
    }));
  }

  connectGitHubRepo(
    projectId: string,
    organizationId: string,
    repositoryId: string,
    installationId: string
  ) {
    const getRepository = () =>
      fromPromise(
        this.#prismaClient.githubRepository.findFirst({
          where: {
            id: repositoryId,
            installationId,
            installation: {
              organizationId: organizationId,
            },
          },
          select: {
            id: true,
            name: true,
            defaultBranch: true,
          },
        }),
        (error) => ({
          type: "other" as const,
          cause: error,
        })
      ).andThen((repository) => {
        if (!repository) {
          return errAsync({ type: "gh_repository_not_found" as const });
        }
        return okAsync(repository);
      });

    const findExistingConnection = () =>
      fromPromise(
        this.#prismaClient.connectedGithubRepository.findFirst({
          where: {
            projectId: projectId,
          },
        }),
        (error) => ({ type: "other" as const, cause: error })
      );

    const createConnectedRepo = (defaultBranch: string, previewDeploymentsEnabled: boolean) =>
      fromPromise(
        this.#prismaClient.connectedGithubRepository.create({
          data: {
            projectId: projectId,
            repositoryId: repositoryId,
            branchTracking: {
              prod: { branch: defaultBranch },
              staging: {},
            } satisfies BranchTrackingConfig,
            previewDeploymentsEnabled,
          },
        }),
        (error) => ({ type: "other" as const, cause: error })
      );

    return ResultAsync.combine([
      getRepository(),
      findExistingConnection(),
      this.isPreviewEnvironmentEnabled(projectId),
    ]).andThen(([repository, existingConnection, previewEnvironmentEnabled]) => {
      if (existingConnection) {
        return errAsync({ type: "project_already_has_connected_repository" as const });
      }

      return createConnectedRepo(repository.defaultBranch, previewEnvironmentEnabled);
    });
  }

  disconnectGitHubRepo(projectId: string) {
    return fromPromise(
      this.#prismaClient.connectedGithubRepository.delete({
        where: {
          projectId: projectId,
        },
      }),
      (error) => ({ type: "other" as const, cause: error })
    );
  }

  updateGitSettings(
    projectId: string,
    productionBranch?: string,
    stagingBranch?: string,
    previewDeploymentsEnabled?: boolean
  ) {
    const getExistingConnectedRepo = () =>
      fromPromise(
        this.#prismaClient.connectedGithubRepository.findFirst({
          where: {
            projectId: projectId,
          },
          include: {
            repository: {
              include: {
                installation: true,
              },
            },
          },
        }),
        (error) => ({ type: "other" as const, cause: error })
      )
        .andThen((connectedRepo) => {
          if (!connectedRepo) {
            return errAsync({ type: "connected_gh_repository_not_found" as const });
          }
          return okAsync(connectedRepo);
        })
        .map((connectedRepo) => {
          const branchTrackingOrFailure = BranchTrackingConfigSchema.safeParse(
            connectedRepo.branchTracking
          );
          const branchTracking = branchTrackingOrFailure.success
            ? branchTrackingOrFailure.data
            : undefined;

          return {
            ...connectedRepo,
            branchTracking,
          };
        });

    const validateProductionBranch = ({
      installationId,
      fullRepoName,
      oldProductionBranch,
    }: {
      installationId: number;
      fullRepoName: string;
      oldProductionBranch?: string;
    }) => {
      if (productionBranch && oldProductionBranch !== productionBranch) {
        return checkGitHubBranchExists(installationId, fullRepoName, productionBranch).andThen(
          (exists) => {
            if (!exists) {
              return errAsync({ type: "production_tracking_branch_not_found" as const });
            }
            return okAsync(productionBranch);
          }
        );
      }

      return okAsync(productionBranch);
    };

    const validateStagingBranch = ({
      installationId,
      fullRepoName,
      oldStagingBranch,
    }: {
      installationId: number;
      fullRepoName: string;
      oldStagingBranch?: string;
    }) => {
      if (stagingBranch && oldStagingBranch !== stagingBranch) {
        return checkGitHubBranchExists(installationId, fullRepoName, stagingBranch).andThen(
          (exists) => {
            if (!exists) {
              return errAsync({ type: "staging_tracking_branch_not_found" as const });
            }
            return okAsync(stagingBranch);
          }
        );
      }

      return okAsync(stagingBranch);
    };

    const updateConnectedRepo = (data: {
      productionBranch: string | undefined;
      stagingBranch: string | undefined;
      previewDeploymentsEnabled: boolean | undefined;
    }) =>
      fromPromise(
        this.#prismaClient.connectedGithubRepository.update({
          where: {
            projectId: projectId,
          },
          data: {
            branchTracking: {
              prod: data.productionBranch ? { branch: data.productionBranch } : {},
              staging: data.stagingBranch ? { branch: data.stagingBranch } : {},
            } satisfies BranchTrackingConfig,
            previewDeploymentsEnabled: data.previewDeploymentsEnabled,
          },
        }),
        (error) => ({ type: "other" as const, cause: error })
      );

    return getExistingConnectedRepo()
      .andThen((connectedRepo) => {
        const installationId = Number(connectedRepo.repository.installation.appInstallationId);

        return ResultAsync.combine([
          validateProductionBranch({
            installationId,
            fullRepoName: connectedRepo.repository.fullName,
            oldProductionBranch: connectedRepo.branchTracking?.prod?.branch,
          }),
          validateStagingBranch({
            installationId,
            fullRepoName: connectedRepo.repository.fullName,
            oldStagingBranch: connectedRepo.branchTracking?.staging?.branch,
          }),
          this.isPreviewEnvironmentEnabled(projectId),
        ]);
      })
      .map(([productionBranch, stagingBranch, previewEnvironmentEnabled]) => ({
        productionBranch,
        stagingBranch,
        previewDeploymentsEnabled: previewDeploymentsEnabled && previewEnvironmentEnabled,
      }))
      .andThen(updateConnectedRepo);
  }

  updateBuildSettings(projectId: string, buildSettings: BuildSettings) {
    return fromPromise(
      this.#prismaClient.project.update({
        where: {
          id: projectId,
        },
        data: {
          buildSettings: buildSettings,
        },
      }),
      (error) => ({
        type: "other" as const,
        cause: error,
      })
    );
  }

  verifyProjectMembership(organizationSlug: string, projectSlug: string, userId: string) {
    const findProject = () =>
      fromPromise(
        this.#prismaClient.project.findFirst({
          where: {
            slug: projectSlug,
            organization: {
              slug: organizationSlug,
              members: {
                some: {
                  userId,
                },
              },
            },
          },
          select: {
            id: true,
            organizationId: true,
          },
        }),
        (error) => ({ type: "other" as const, cause: error })
      );

    return findProject().andThen((project) => {
      if (!project) {
        return errAsync({ type: "user_not_in_project" as const });
      }

      return okAsync({
        projectId: project.id,
        organizationId: project.organizationId,
      });
    });
  }

  private isPreviewEnvironmentEnabled(projectId: string) {
    return fromPromise(
      this.#prismaClient.runtimeEnvironment.findFirst({
        select: {
          id: true,
        },
        where: {
          projectId: projectId,
          slug: "preview",
        },
      }),
      (error) => ({
        type: "other" as const,
        cause: error,
      })
    ).map((previewEnvironment) => previewEnvironment !== null);
  }
}
