import type { PrismaClient, Prisma, ResourceProposal } from '@prisma/client';

import type {
  IResourceProposalRepository,
  CreateProposalInput,
  ProposalListFilter,
} from '../repositories/resource-proposal.repository.js';
import type { ResourceType } from '../repositories/resource-revision.repository.js';
import { NotFoundError } from './mcp-server.service.js';

/**
 * Per-resourceType handler invoked when a proposal is approved. The
 * handler runs inside the approval transaction; it must apply the
 * proposed body to the live resource (creating it if needed), record
 * a ResourceRevision, and return the resulting revision id so the
 * proposal row can link to it.
 *
 * Registered by the resource's own service at boot time:
 *   PromptService → setHandler('prompt', ...)
 *   SkillService  → setHandler('skill',  ...)   // PR-3
 */
export type ProposalApprovalHandler = (
  proposal: ResourceProposal,
  tx: Prisma.TransactionClient,
  approverUserId?: string,
) => Promise<{ resourceId: string; revisionId: string }>;

export interface ProposeInput {
  resourceType: ResourceType;
  name: string;
  body: Record<string, unknown>;
  projectId?: string;
  agentId?: string;
  createdBySession?: string;
  createdByUserId?: string;
}

export class ResourceProposalService {
  private readonly handlers = new Map<ResourceType, ProposalApprovalHandler>();

  constructor(
    private readonly repo: IResourceProposalRepository,
    private readonly prisma: PrismaClient,
  ) {}

  /** Registered by Prompt/Skill services at construction time. */
  setHandler(resourceType: ResourceType, handler: ProposalApprovalHandler): void {
    this.handlers.set(resourceType, handler);
  }

  async list(filter: ProposalListFilter): Promise<ResourceProposal[]> {
    return this.repo.list(filter);
  }

  async getById(id: string): Promise<ResourceProposal> {
    const proposal = await this.repo.findById(id);
    if (proposal === null) throw new NotFoundError(`Proposal not found: ${id}`);
    return proposal;
  }

  async findBySession(sessionId: string, projectId?: string): Promise<ResourceProposal[]> {
    return this.repo.findBySession(sessionId, projectId);
  }

  async propose(input: ProposeInput): Promise<ResourceProposal> {
    const data: CreateProposalInput = {
      resourceType: input.resourceType,
      name: input.name,
      body: input.body as Prisma.InputJsonValue,
    };
    if (input.projectId !== undefined) data.projectId = input.projectId;
    if (input.agentId !== undefined) data.agentId = input.agentId;
    if (input.createdBySession !== undefined) data.createdBySession = input.createdBySession;
    if (input.createdByUserId !== undefined) data.createdByUserId = input.createdByUserId;
    return this.repo.create(data);
  }

  async updateBody(id: string, body: Record<string, unknown>): Promise<ResourceProposal> {
    await this.getById(id); // 404 if missing
    return this.repo.updateBody(id, body as Prisma.InputJsonValue);
  }

  /**
   * Approve the proposal: dispatch to the type-specific handler inside
   * a transaction, then mark the proposal `approved` and link the
   * resulting revision id.
   */
  async approve(id: string, approverUserId?: string): Promise<ResourceProposal> {
    return this.prisma.$transaction(async (tx) => {
      const proposal = await tx.resourceProposal.findUnique({ where: { id } });
      if (proposal === null) throw new NotFoundError(`Proposal not found: ${id}`);
      if (proposal.status !== 'pending') {
        throw Object.assign(
          new Error(`Proposal is ${proposal.status}, not pending`),
          { statusCode: 409 },
        );
      }
      const handler = this.handlers.get(proposal.resourceType as ResourceType);
      if (handler === undefined) {
        throw Object.assign(
          new Error(`No approval handler registered for resource type: ${proposal.resourceType}`),
          { statusCode: 500 },
        );
      }
      const { revisionId } = await handler(proposal, tx, approverUserId);
      return tx.resourceProposal.update({
        where: { id },
        data: {
          status: 'approved',
          approvedRevisionId: revisionId,
          version: { increment: 1 },
          ...(approverUserId !== undefined ? {} : {}),
        },
      });
    });
  }

  async reject(id: string, reviewerNote: string, _reviewerUserId?: string): Promise<ResourceProposal> {
    const proposal = await this.getById(id);
    if (proposal.status !== 'pending') {
      throw Object.assign(
        new Error(`Proposal is ${proposal.status}, not pending`),
        { statusCode: 409 },
      );
    }
    return this.repo.updateStatus(id, { status: 'rejected', reviewerNote });
  }

  async delete(id: string): Promise<void> {
    await this.getById(id);
    await this.repo.delete(id);
  }
}
