import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import type { GiteaPrWebhookDto } from "./dto/gitea-pr-webhook.dto";

export interface ReviewResult {
  passed: boolean;
  issues: string[];
}

@Injectable()
export class GatekeeperService {
  private readonly logger = new Logger(GatekeeperService.name);

  private get giteaApiBaseUrl(): string {
    return `${this.configService.getOrThrow<string>("GITEA_URL")}/api/v1`;
  }

  constructor(
    private readonly prisma: PrismaService,
    private readonly configService: ConfigService
  ) {}

  async handlePrEvent(payload: GiteaPrWebhookDto): Promise<void> {
    if (!this.isEnabled()) {
      return;
    }

    if (payload.type !== "pull_request") {
      return;
    }

    const action = payload.action;
    const hasAutoMergeLabel = this.hasAutoMergeLabel(payload);

    if (!["opened", "labeled", "synchronize"].includes(action)) {
      return;
    }

    if (action === "labeled" && payload.label?.name !== "auto-merge") {
      return;
    }

    const merge = await this.prisma.pendingMerge.upsert({
      where: {
        repo_prNumber_headSha: {
          repo: payload.repository.full_name,
          prNumber: payload.pull_request.number,
          headSha: payload.pull_request.head.sha,
        },
      },
      create: {
        repo: payload.repository.full_name,
        prNumber: payload.pull_request.number,
        headSha: payload.pull_request.head.sha,
        ...(payload.sender?.login ? { requester: payload.sender.login } : {}),
        giteaMergeUrl:
          payload.pull_request.url ??
          `${this.giteaApiBaseUrl}/repos/${payload.repository.full_name}/pulls/${String(payload.pull_request.number)}`,
      },
      update: {
        headSha: payload.pull_request.head.sha,
        ...(payload.sender?.login ? { requester: payload.sender.login } : {}),
        giteaMergeUrl:
          payload.pull_request.url ??
          `${this.giteaApiBaseUrl}/repos/${payload.repository.full_name}/pulls/${String(payload.pull_request.number)}`,
      },
    });

    if (action === "synchronize") {
      await this.prisma.pendingMerge.update({
        where: { id: merge.id },
        data: {
          state: hasAutoMergeLabel ? "pending" : "rejected",
          ciStatus: null,
          reviewResult: Prisma.DbNull,
        },
      });

      if (hasAutoMergeLabel) {
        await this.runReview(merge.id, payload);
      }

      return;
    }

    if (hasAutoMergeLabel) {
      await this.runReview(merge.id, payload);
    }
  }

  async handleCiEvent(
    repo: string,
    prNumber: number,
    headSha: string,
    status: "success" | "failure"
  ): Promise<void> {
    if (!this.isEnabled()) {
      return;
    }

    const merge = await this.prisma.pendingMerge.findFirst({
      where: {
        repo,
        prNumber,
        headSha,
      },
      orderBy: {
        createdAt: "desc",
      },
    });

    if (!merge) {
      this.logger.debug(`No pending merge found for ${repo}#${String(prNumber)} @ ${headSha}`);
      return;
    }

    await this.prisma.pendingMerge.update({
      where: { id: merge.id },
      data: {
        ciStatus: status,
      },
    });

    if (status === "failure") {
      await this.rejectMerge(merge.id, "CI reported failure");
      return;
    }

    if (merge.state === "awaiting_ci") {
      await this.attemptMerge(merge.id);
    }
  }

  reviewPr(payload: GiteaPrWebhookDto): Promise<ReviewResult> {
    const issues: string[] = [];

    if (!this.hasAutoMergeLabel(payload)) {
      issues.push("PR must have the auto-merge label");
    }

    if (!payload.pull_request.body?.trim()) {
      issues.push("PR description must not be empty");
    }

    if (payload.pull_request.base.ref !== "main") {
      issues.push("PR base branch must be main");
    }

    if (!/^[0-9a-f]{7,128}$/i.test(payload.pull_request.head.sha)) {
      issues.push("PR head SHA must be a valid git commit hash");
    }

    return Promise.resolve({
      passed: issues.length === 0,
      issues,
    });
  }

  async attemptMerge(mergeId: string): Promise<void> {
    const merge = await this.prisma.pendingMerge.findUnique({
      where: { id: mergeId },
    });

    if (!merge) {
      return;
    }

    const token = this.configService.get<string>("GITEA_API_TOKEN");
    if (!token) {
      await this.rejectMerge(merge.id, "GITEA_API_TOKEN is not configured");
      return;
    }

    await this.prisma.pendingMerge.update({
      where: { id: merge.id },
      data: { state: "merging" },
    });

    const mergeUrl =
      merge.giteaMergeUrl ??
      `${this.giteaApiBaseUrl}/repos/${merge.repo}/pulls/${String(merge.prNumber)}`;
    const response = await fetch(`${mergeUrl}/merge`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${token}`,
      },
      body: JSON.stringify({
        Do: "merge",
        force_merge: true,
        merge_message_field: "Auto-merged by Gatekeeper",
      }),
    });

    if (!response.ok) {
      const reason = await response.text();
      await this.rejectMerge(
        merge.id,
        `Gitea merge API rejected the request: ${String(response.status)} ${reason}`
      );
      return;
    }

    await this.prisma.pendingMerge.update({
      where: { id: merge.id },
      data: { state: "merged" },
    });
  }

  async rejectMerge(mergeId: string, reason: string): Promise<void> {
    const merge = await this.prisma.pendingMerge.findUnique({
      where: { id: mergeId },
    });

    if (!merge) {
      return;
    }

    await this.prisma.pendingMerge.update({
      where: { id: merge.id },
      data: {
        state: "rejected",
        reviewResult: {
          passed: false,
          issues: [reason],
        },
      },
    });

    await this.postPullRequestComment(
      merge.repo,
      merge.prNumber,
      `Gatekeeper rejected auto-merge for \`${merge.headSha}\`: ${reason}`
    );
  }

  private async runReview(mergeId: string, payload: GiteaPrWebhookDto): Promise<void> {
    await this.prisma.pendingMerge.update({
      where: { id: mergeId },
      data: { state: "reviewing" },
    });

    const result = await this.reviewPr(payload);
    if (!result.passed) {
      await this.rejectMerge(mergeId, result.issues.join("; "));
      return;
    }

    const reviewResult: Prisma.InputJsonValue = {
      passed: result.passed,
      issues: result.issues,
    };

    await this.prisma.pendingMerge.update({
      where: { id: mergeId },
      data: {
        state: "awaiting_ci",
        reviewResult,
      },
    });
  }

  private hasAutoMergeLabel(payload: GiteaPrWebhookDto): boolean {
    return payload.pull_request.labels.some((label) => label.name === "auto-merge");
  }

  private async postPullRequestComment(
    repo: string,
    prNumber: number,
    body: string
  ): Promise<void> {
    const token = this.configService.get<string>("GITEA_API_TOKEN");
    if (!token) {
      this.logger.warn(
        `Skipping PR comment for ${repo}#${String(prNumber)}; GITEA_API_TOKEN is missing`
      );
      return;
    }

    const response = await fetch(
      `${this.giteaApiBaseUrl}/repos/${repo}/issues/${String(prNumber)}/comments`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `token ${token}`,
        },
        body: JSON.stringify({ body }),
      }
    );

    if (!response.ok) {
      this.logger.warn(
        `Failed to post Gatekeeper PR comment for ${repo}#${String(prNumber)}: ${String(response.status)}`
      );
    }
  }

  private isEnabled(): boolean {
    const raw = this.configService.get<string>("GATEKEEPER_ENABLED");
    const enabled = raw !== "false";

    if (!enabled) {
      this.logger.warn("Gatekeeper is disabled via GATEKEEPER_ENABLED");
    }

    return enabled;
  }
}
