import { Injectable, Logger, NotFoundException, BadRequestException } from "@nestjs/common";
import { Prisma, RunnerJobStatus } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import { JobEventsService } from "../job-events/job-events.service";
import { HeraldService } from "../herald/herald.service";
import { BullMqService } from "../bullmq/bullmq.service";
import { QUEUE_NAMES } from "../bullmq/queues";
import { JOB_PROGRESS } from "../job-events/event-types";
import { ConcurrentUpdateException } from "../common/exceptions/concurrent-update.exception";
import {
  CoordinatorJobStatus,
  type CreateCoordinatorJobDto,
  type UpdateJobStatusDto,
  type UpdateJobProgressDto,
  type CompleteJobDto,
  type FailJobDto,
} from "./dto";
import type { CoordinatorJobResult, CoordinatorHealthStatus } from "./interfaces";

/**
 * CoordinatorIntegrationService - Bridge between Python coordinator and NestJS API
 *
 * Responsibilities:
 * - Create jobs from coordinator webhook events
 * - Update job status as coordinator processes
 * - Handle job completion and failure
 * - Broadcast events via Herald
 * - Provide integration health status
 */
@Injectable()
export class CoordinatorIntegrationService {
  private readonly logger = new Logger(CoordinatorIntegrationService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly jobEvents: JobEventsService,
    private readonly herald: HeraldService,
    private readonly bullMq: BullMqService
  ) {}

  /**
   * Create a job from the coordinator
   */
  async createJob(dto: CreateCoordinatorJobDto): Promise<CoordinatorJobResult> {
    this.logger.log(`Creating job for issue #${String(dto.issueNumber)} from ${dto.repository}`);

    // Verify workspace exists
    const workspace = await this.prisma.workspace.findUnique({
      where: { id: dto.workspaceId },
      select: { id: true },
    });

    if (!workspace) {
      throw new NotFoundException(`Workspace with ID ${dto.workspaceId} not found`);
    }

    // Create RunnerJob in database
    const job = await this.prisma.runnerJob.create({
      data: {
        workspaceId: dto.workspaceId,
        type: dto.type,
        priority: dto.priority ?? 10,
        status: RunnerJobStatus.PENDING,
        progressPercent: 0,
      },
    });

    // Emit job.created event
    await this.jobEvents.emitJobCreated(job.id, {
      issueNumber: dto.issueNumber,
      repository: dto.repository,
      type: dto.type,
      priority: dto.priority ?? 10,
      metadata: dto.metadata,
      source: "coordinator",
    });

    // Add job to BullMQ queue
    await this.bullMq.addJob(
      QUEUE_NAMES.MAIN,
      dto.type,
      {
        jobId: job.id,
        workspaceId: dto.workspaceId,
        issueNumber: dto.issueNumber,
        repository: dto.repository,
        metadata: dto.metadata,
      },
      { priority: dto.priority ?? 10 }
    );

    this.logger.log(`Job ${job.id} created and queued for issue #${String(dto.issueNumber)}`);

    return {
      jobId: job.id,
      status: job.status,
      queueName: QUEUE_NAMES.MAIN,
    };
  }

  /**
   * Update job status from the coordinator using transaction with SELECT FOR UPDATE
   * This ensures serialized access to job status updates from the coordinator
   */
  async updateJobStatus(
    jobId: string,
    dto: UpdateJobStatusDto
  ): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.update>>> {
    this.logger.log(`Updating job ${jobId} status to ${dto.status}`);

    return this.prisma.$transaction(async (tx) => {
      // Use SELECT FOR UPDATE to lock the row during this transaction
      // This prevents concurrent updates from coordinator and ensures serialization
      const jobs = await tx.$queryRaw<
        { id: string; status: RunnerJobStatus; workspace_id: string; version: number }[]
      >`
        SELECT id, status, workspace_id, version
        FROM runner_jobs
        WHERE id = ${jobId}::uuid
        FOR UPDATE
      `;

      if (jobs.length === 0) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      const job = jobs[0];
      if (!job) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      // Validate status transition
      if (!this.isValidStatusTransition(job.status, dto.status as RunnerJobStatus)) {
        throw new BadRequestException(
          `Invalid status transition from ${job.status} to ${dto.status}`
        );
      }

      const updateData: Prisma.RunnerJobUpdateInput = {
        status: dto.status as RunnerJobStatus,
        version: { increment: 1 },
      };

      // Set startedAt when transitioning to RUNNING
      if (dto.status === CoordinatorJobStatus.RUNNING) {
        updateData.startedAt = new Date();
      }

      const updatedJob = await tx.runnerJob.update({
        where: { id: jobId },
        data: updateData,
      });

      // Emit appropriate event (outside of critical section but inside transaction)
      if (dto.status === CoordinatorJobStatus.RUNNING) {
        const event = await this.jobEvents.emitJobStarted(jobId, {
          agentId: dto.agentId,
          agentType: dto.agentType,
        });

        // Broadcast via Herald
        await this.herald.broadcastJobEvent(jobId, event);
      }

      return updatedJob;
    });
  }

  /**
   * Update job progress from the coordinator with optimistic locking
   */
  async updateJobProgress(
    jobId: string,
    dto: UpdateJobProgressDto
  ): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.findUnique>>> {
    this.logger.log(`Updating job ${jobId} progress to ${String(dto.progressPercent)}%`);

    // Read current job state
    const job = await this.prisma.runnerJob.findUnique({
      where: { id: jobId },
      select: { id: true, status: true, version: true },
    });

    if (!job) {
      throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
    }

    if (job.status !== RunnerJobStatus.RUNNING) {
      throw new BadRequestException(`Cannot update progress for job with status ${job.status}`);
    }

    // Use updateMany with version check for optimistic locking
    const result = await this.prisma.runnerJob.updateMany({
      where: {
        id: jobId,
        version: job.version,
      },
      data: {
        progressPercent: dto.progressPercent,
        version: { increment: 1 },
      },
    });

    if (result.count === 0) {
      throw new ConcurrentUpdateException("RunnerJob", jobId, job.version);
    }

    // Fetch updated job
    const updatedJob = await this.prisma.runnerJob.findUnique({
      where: { id: jobId },
    });

    if (!updatedJob) {
      throw new NotFoundException(`RunnerJob with ID ${jobId} not found after update`);
    }

    // Emit progress event
    await this.jobEvents.emitEvent(jobId, {
      type: JOB_PROGRESS,
      actor: "coordinator",
      payload: {
        progressPercent: dto.progressPercent,
        currentStep: dto.currentStep,
        tokensUsed: dto.tokensUsed,
      },
    });

    return updatedJob;
  }

  /**
   * Mark job as completed from the coordinator using transaction with SELECT FOR UPDATE
   */
  async completeJob(
    jobId: string,
    dto: CompleteJobDto
  ): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.update>>> {
    this.logger.log(`Completing job ${jobId}`);

    return this.prisma.$transaction(async (tx) => {
      // Lock the row to prevent concurrent completion/failure
      const jobs = await tx.$queryRaw<
        { id: string; status: RunnerJobStatus; started_at: Date | null; version: number }[]
      >`
        SELECT id, status, started_at, version
        FROM runner_jobs
        WHERE id = ${jobId}::uuid
        FOR UPDATE
      `;

      if (jobs.length === 0) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      const job = jobs[0];
      if (!job) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      // Validate status transition
      if (!this.isValidStatusTransition(job.status, RunnerJobStatus.COMPLETED)) {
        throw new BadRequestException(`Cannot complete job with status ${job.status}`);
      }

      // Calculate duration if not provided
      let durationSeconds = dto.durationSeconds;
      if (durationSeconds === undefined && job.started_at) {
        durationSeconds = Math.round(
          (new Date().getTime() - new Date(job.started_at).getTime()) / 1000
        );
      }

      const updateData: Prisma.RunnerJobUpdateInput = {
        status: RunnerJobStatus.COMPLETED,
        progressPercent: 100,
        completedAt: new Date(),
        version: { increment: 1 },
      };

      if (dto.result) {
        updateData.result = dto.result as Prisma.InputJsonValue;
      }

      const updatedJob = await tx.runnerJob.update({
        where: { id: jobId },
        data: updateData,
      });

      // Emit completion event
      const event = await this.jobEvents.emitJobCompleted(jobId, {
        result: dto.result,
        tokensUsed: dto.tokensUsed,
        durationSeconds,
      });

      // Broadcast via Herald
      await this.herald.broadcastJobEvent(jobId, event);

      return updatedJob;
    });
  }

  /**
   * Mark job as failed from the coordinator using transaction with SELECT FOR UPDATE
   */
  async failJob(
    jobId: string,
    dto: FailJobDto
  ): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.update>>> {
    this.logger.log(`Failing job ${jobId}: ${dto.error}`);

    return this.prisma.$transaction(async (tx) => {
      // Lock the row to prevent concurrent completion/failure
      const jobs = await tx.$queryRaw<{ id: string; status: RunnerJobStatus; version: number }[]>`
        SELECT id, status, version
        FROM runner_jobs
        WHERE id = ${jobId}::uuid
        FOR UPDATE
      `;

      if (jobs.length === 0) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      const job = jobs[0];
      if (!job) {
        throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
      }

      // Validate status transition
      if (!this.isValidStatusTransition(job.status, RunnerJobStatus.FAILED)) {
        throw new BadRequestException(`Cannot fail job with status ${job.status}`);
      }

      const updatedJob = await tx.runnerJob.update({
        where: { id: jobId },
        data: {
          status: RunnerJobStatus.FAILED,
          error: dto.error,
          completedAt: new Date(),
          version: { increment: 1 },
        },
      });

      // Emit failure event
      const event = await this.jobEvents.emitJobFailed(jobId, {
        error: dto.error,
        gateResults: dto.gateResults,
        failedStep: dto.failedStep,
        continuationPrompt: dto.continuationPrompt,
      });

      // Broadcast via Herald
      await this.herald.broadcastJobEvent(jobId, event);

      return updatedJob;
    });
  }

  /**
   * Get job details with events and steps
   */
  async getJobDetails(
    jobId: string
  ): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.findUnique>>> {
    const job = await this.prisma.runnerJob.findUnique({
      where: { id: jobId },
      include: {
        steps: {
          orderBy: { ordinal: "asc" },
        },
        events: {
          orderBy: { timestamp: "asc" },
        },
      },
    });

    if (!job) {
      throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
    }

    return job;
  }

  /**
   * Get integration health status
   */
  async getIntegrationHealth(): Promise<CoordinatorHealthStatus> {
    let bullmqStatus = { connected: false, queues: {} as Record<string, number> };

    try {
      bullmqStatus = await this.bullMq.getHealthStatus();
    } catch (error) {
      this.logger.error("Failed to get BullMQ health status", error);
    }

    return {
      api: true,
      bullmq: bullmqStatus,
      timestamp: new Date(),
    };
  }

  /**
   * Validate status transitions
   */
  private isValidStatusTransition(
    currentStatus: RunnerJobStatus,
    newStatus: RunnerJobStatus
  ): boolean {
    // Define valid transitions
    const validTransitions: Record<RunnerJobStatus, RunnerJobStatus[]> = {
      [RunnerJobStatus.PENDING]: [
        RunnerJobStatus.QUEUED,
        RunnerJobStatus.RUNNING,
        RunnerJobStatus.CANCELLED,
      ],
      [RunnerJobStatus.QUEUED]: [RunnerJobStatus.RUNNING, RunnerJobStatus.CANCELLED],
      [RunnerJobStatus.RUNNING]: [
        RunnerJobStatus.COMPLETED,
        RunnerJobStatus.FAILED,
        RunnerJobStatus.CANCELLED,
      ],
      [RunnerJobStatus.COMPLETED]: [],
      [RunnerJobStatus.FAILED]: [],
      [RunnerJobStatus.CANCELLED]: [],
    };

    return validTransitions[currentStatus].includes(newStatus);
  }
}
