import type { Prompt, PromptRequest } from '@prisma/client';
import type { IPromptRepository } from '../repositories/prompt.repository.js';
import type { IPromptRequestRepository } from '../repositories/prompt-request.repository.js';
import type { IProjectRepository } from '../repositories/project.repository.js';
import type { IAgentRepository } from '../repositories/agent.repository.js';
import { CreatePromptSchema, UpdatePromptSchema, CreatePromptRequestSchema, UpdatePromptRequestSchema } from '../validation/prompt.schema.js';
import { NotFoundError } from './mcp-server.service.js';
import type { PromptSummaryService } from './prompt-summary.service.js';
import type { ResourceRevisionService } from './resource-revision.service.js';
import type { ResourceProposalService } from './resource-proposal.service.js';
import { bumpSemver, type BumpKind } from '../utils/semver.js';
import { SYSTEM_PROJECT_NAME, getSystemPromptDefault } from '../bootstrap/system-project.js';
import type { ResourceRuleRegistry, RuleContext } from '../validation/resource-rules.js';

export class PromptService {
  private summaryService: PromptSummaryService | null = null;
  private revisionService: ResourceRevisionService | null = null;

  constructor(
    private readonly promptRepo: IPromptRepository,
    private readonly promptRequestRepo: IPromptRequestRepository,
    private readonly projectRepo: IProjectRepository,
    private readonly ruleRegistry?: ResourceRuleRegistry,
    private readonly agentRepo?: IAgentRepository,
  ) {}

  setSummaryService(service: PromptSummaryService): void {
    this.summaryService = service;
  }

  /**
   * Wire revision + proposal infrastructure (PR-2). Optional so existing
   * tests that construct a bare PromptService keep working unchanged —
   * when these are unset, create/update skip the revision write and
   * proposal-approval is unsupported.
   */
  setRevisionService(service: ResourceRevisionService): void {
    this.revisionService = service;
  }

  setProposalService(service: ResourceProposalService): void {
    // Register a 'prompt' approval handler so proposalService.approve(id)
    // can dispatch to us when the proposal targets a prompt. The service
    // itself is kept only via this closure binding — no field needed.
    service.setHandler('prompt', async (proposal, tx, _approverUserId) => {
      const body = (proposal.body ?? {}) as Record<string, unknown>;
      const content = String(body['content'] ?? '');
      const priority = typeof body['priority'] === 'number' ? body['priority'] : 5;
      const linkTarget = typeof body['linkTarget'] === 'string' ? body['linkTarget'] : undefined;
      // Resolve scope: project-only for now (agent-scoped proposals come with PR-3+).
      const projectId = proposal.projectId ?? null;
      const agentId = proposal.agentId ?? null;

      // Upsert: existing prompt with this (name, scope) → update body and bump semver;
      // otherwise → create at 0.1.0.
      const existing = agentId !== null
        ? await tx.prompt.findUnique({ where: { name_agentId: { name: proposal.name, agentId } } })
        : await tx.prompt.findUnique({ where: { name_projectId: { name: proposal.name, projectId: projectId ?? '' } } });

      let promptId: string;
      let newSemver: string;
      if (existing !== null) {
        // Bump patch for an approved-update.
        newSemver = bumpSemver(existing.semver, 'patch');
        await tx.prompt.update({
          where: { id: existing.id },
          data: { content, priority, semver: newSemver },
        });
        promptId = existing.id;
      } else {
        // Approval-from-scratch: prompt didn't exist before this proposal.
        newSemver = '0.1.0';
        const created = await tx.prompt.create({
          data: {
            name: proposal.name,
            content,
            priority,
            ...(projectId !== null ? { projectId } : {}),
            ...(agentId !== null ? { agentId } : {}),
            ...(linkTarget !== undefined ? { linkTarget } : {}),
            semver: newSemver,
          },
        });
        promptId = created.id;
      }

      const { revision } = await this.revisionService!.record(
        {
          resourceType: 'prompt',
          resourceId: promptId,
          semver: newSemver,
          body: { content, priority, ...(linkTarget !== undefined ? { linkTarget } : {}) },
          ...(proposal.createdByUserId !== null ? { authorUserId: proposal.createdByUserId } : {}),
          ...(proposal.createdBySession !== null ? { authorSessionId: proposal.createdBySession } : {}),
          note: `approved proposal ${proposal.id}`,
        },
        tx,
      );

      // Soft pointer to latest revision.
      await tx.prompt.update({
        where: { id: promptId },
        data: { currentRevisionId: revision.id },
      });

      return { resourceId: promptId, revisionId: revision.id };
    });
  }

  /**
   * Run resource validation rules for a prompt.
   * Throws 400 if validation fails.
   */
  private async validatePromptRules(
    name: string,
    content: string,
    projectId: string | undefined | null,
    operation: 'create' | 'update',
  ): Promise<void> {
    if (!this.ruleRegistry || !projectId) return;

    const project = await this.projectRepo.findById(projectId);
    const isSystem = project?.name === SYSTEM_PROJECT_NAME;

    const ctx: RuleContext = {
      findResource: async () => null,
    };

    const result = await this.ruleRegistry.validate(
      'prompt',
      { name, content },
      { projectName: project?.name, isSystemResource: isSystem, operation },
      ctx,
    );

    if (!result.valid) {
      throw Object.assign(
        new Error(result.errors?.join('; ') ?? 'Validation failed'),
        { statusCode: 400 },
      );
    }
  }

  // ── Prompt CRUD ──

  async listPrompts(projectId?: string): Promise<Prompt[]> {
    return this.promptRepo.findAll(projectId);
  }

  async listGlobalPrompts(): Promise<Prompt[]> {
    return this.promptRepo.findGlobal();
  }

  async listPromptsForAgent(agentId: string): Promise<Prompt[]> {
    return this.promptRepo.findByAgent(agentId);
  }

  async getPrompt(id: string): Promise<Prompt> {
    const prompt = await this.promptRepo.findById(id);
    if (prompt === null) throw new NotFoundError(`Prompt not found: ${id}`);
    return prompt;
  }

  async createPrompt(input: unknown): Promise<Prompt> {
    const data = CreatePromptSchema.parse(input);

    if (data.projectId !== undefined) {
      const project = await this.projectRepo.findById(data.projectId);
      if (project === null) throw new NotFoundError(`Project not found: ${data.projectId}`);
    }
    if (data.agentId !== undefined) {
      if (this.agentRepo === undefined) {
        throw new Error('Agent-scoped prompts require AgentRepository to be wired into PromptService');
      }
      const agent = await this.agentRepo.findById(data.agentId);
      if (agent === null) throw new NotFoundError(`Agent not found: ${data.agentId}`);
    }

    await this.validatePromptRules(data.name, data.content, data.projectId, 'create');

    const createData: { name: string; content: string; projectId?: string; agentId?: string; priority?: number; linkTarget?: string } = {
      name: data.name,
      content: data.content,
    };
    if (data.projectId !== undefined) createData.projectId = data.projectId;
    if (data.agentId !== undefined) createData.agentId = data.agentId;
    if (data.priority !== undefined) createData.priority = data.priority;
    if (data.linkTarget !== undefined) createData.linkTarget = data.linkTarget;
    const prompt = await this.promptRepo.create(createData);
    // Record initial revision (0.1.0). Non-blocking — revision is audit, not source of truth.
    if (this.revisionService) {
      this.recordPromptRevision(prompt, '0.1.0', 'created').catch(() => {});
    }
    // Auto-generate summary/chapters (non-blocking — don't fail create if summary fails)
    if (this.summaryService && !data.linkTarget) {
      this.generateAndStoreSummary(prompt.id, data.content).catch(() => {});
    }
    return prompt;
  }

  async updatePrompt(id: string, input: unknown): Promise<Prompt> {
    const data = UpdatePromptSchema.parse(input);
    if (data.semver !== undefined && data.bump !== undefined) {
      throw Object.assign(new Error('Pass --semver or --bump, not both'), { statusCode: 400 });
    }
    const existing = await this.getPrompt(id);

    if (data.content !== undefined) {
      await this.validatePromptRules(existing.name, data.content, existing.projectId, 'update');
    }

    // Resolve new semver:
    //   explicit > explicit-bump > auto-patch (only when content changed)
    let newSemver = existing.semver;
    if (data.semver !== undefined) {
      newSemver = data.semver;
    } else if (data.bump !== undefined) {
      newSemver = bumpSemver(existing.semver, data.bump as BumpKind);
    } else if (data.content !== undefined) {
      newSemver = bumpSemver(existing.semver, 'patch');
    }

    const updateData: { content?: string; priority?: number; semver?: string } = {};
    if (data.content !== undefined) updateData.content = data.content;
    if (data.priority !== undefined) updateData.priority = data.priority;
    if (newSemver !== existing.semver) updateData.semver = newSemver;
    const prompt = await this.promptRepo.update(id, updateData);

    // Record revision when content actually changed OR semver was explicitly bumped.
    const shouldRecord = data.content !== undefined || data.bump !== undefined || data.semver !== undefined;
    if (this.revisionService && shouldRecord) {
      this.recordPromptRevision(prompt, newSemver, data.note ?? null).catch(() => {});
    }

    // Regenerate summary when content changes
    if (this.summaryService && data.content !== undefined && !prompt.linkTarget) {
      this.generateAndStoreSummary(prompt.id, data.content).catch(() => {});
    }
    return prompt;
  }

  /**
   * Append a ResourceRevision row for this prompt and update its
   * currentRevisionId. Best-effort — failures are swallowed because the
   * audit log isn't load-bearing (the resource row's inline content is
   * the source of truth).
   */
  private async recordPromptRevision(prompt: Prompt, semver: string, note: string | null): Promise<void> {
    if (this.revisionService === null) return;
    const body: Record<string, unknown> = { content: prompt.content, priority: prompt.priority };
    if (prompt.linkTarget !== null) body['linkTarget'] = prompt.linkTarget;
    const { revision } = await this.revisionService.record({
      resourceType: 'prompt',
      resourceId: prompt.id,
      semver,
      body,
      ...(note !== null ? { note } : {}),
    });
    await this.promptRepo.update(prompt.id, { currentRevisionId: revision.id });
  }

  /**
   * Restore a prompt to a prior revision: writes the revision's body
   * back as a NEW update (which produces a new patch-bumped revision),
   * preserving the audit chain. Returns the updated prompt.
   */
  async restoreRevisionForPrompt(promptId: string, revisionId: string, note?: string): Promise<Prompt> {
    if (this.revisionService === null) {
      throw new Error('Revision service not wired');
    }
    const revision = await this.revisionService.getById(revisionId);
    if (revision === null) throw new NotFoundError(`Revision not found: ${revisionId}`);
    if (revision.resourceType !== 'prompt' || revision.resourceId !== promptId) {
      throw Object.assign(
        new Error('Revision does not belong to this prompt'),
        { statusCode: 400 },
      );
    }
    const body = (revision.body ?? {}) as Record<string, unknown>;
    const content = typeof body['content'] === 'string' ? body['content'] : undefined;
    const priority = typeof body['priority'] === 'number' ? body['priority'] : undefined;
    if (content === undefined) {
      throw Object.assign(new Error('Revision has no content to restore'), { statusCode: 400 });
    }
    return this.updatePrompt(promptId, {
      content,
      priority,
      bump: 'patch',
      note: note ?? `restored from revision ${revisionId}`,
    });
  }

  async regenerateSummary(id: string): Promise<Prompt> {
    const prompt = await this.getPrompt(id);
    if (!this.summaryService) {
      throw new Error('Summary generation not available');
    }
    return this.generateAndStoreSummary(prompt.id, prompt.content);
  }

  private async generateAndStoreSummary(id: string, content: string): Promise<Prompt> {
    if (!this.summaryService) throw new Error('No summary service');
    const { summary, chapters } = await this.summaryService.generateSummary(content);
    return this.promptRepo.update(id, { summary, chapters });
  }

  async deletePrompt(id: string): Promise<Prompt | void> {
    const prompt = await this.getPrompt(id);
    // System prompts: reset to codebase default instead of deleting
    if (prompt.projectId) {
      const project = await this.projectRepo.findById(prompt.projectId);
      if (project?.name === SYSTEM_PROJECT_NAME) {
        const defaultContent = getSystemPromptDefault(prompt.name);
        if (defaultContent !== undefined) {
          return this.promptRepo.update(id, { content: defaultContent });
        }
        // Unknown system prompt — allow deletion
      }
    }
    await this.promptRepo.delete(id);
  }

  // ── PromptRequest CRUD ──

  async listPromptRequests(projectId?: string): Promise<PromptRequest[]> {
    return this.promptRequestRepo.findAll(projectId);
  }

  async listGlobalPromptRequests(): Promise<PromptRequest[]> {
    return this.promptRequestRepo.findGlobal();
  }

  async getPromptRequest(id: string): Promise<PromptRequest> {
    const req = await this.promptRequestRepo.findById(id);
    if (req === null) throw new NotFoundError(`PromptRequest not found: ${id}`);
    return req;
  }

  async updatePromptRequest(id: string, input: unknown): Promise<PromptRequest> {
    await this.getPromptRequest(id);
    const data = UpdatePromptRequestSchema.parse(input);
    const updateData: { content?: string; priority?: number } = {};
    if (data.content !== undefined) updateData.content = data.content;
    if (data.priority !== undefined) updateData.priority = data.priority;
    return this.promptRequestRepo.update(id, updateData);
  }

  async deletePromptRequest(id: string): Promise<void> {
    await this.getPromptRequest(id);
    await this.promptRequestRepo.delete(id);
  }

  // ── Propose (LLM creates a PromptRequest) ──

  async propose(input: unknown): Promise<PromptRequest> {
    const data = CreatePromptRequestSchema.parse(input);

    if (data.projectId) {
      const project = await this.projectRepo.findById(data.projectId);
      if (project === null) throw new NotFoundError(`Project not found: ${data.projectId}`);
    }

    const createData: { name: string; content: string; projectId?: string; priority?: number; createdBySession?: string; createdByUserId?: string } = {
      name: data.name,
      content: data.content,
    };
    if (data.projectId !== undefined) createData.projectId = data.projectId;
    if (data.priority !== undefined) createData.priority = data.priority;
    if (data.createdBySession !== undefined) createData.createdBySession = data.createdBySession;
    if (data.createdByUserId !== undefined) createData.createdByUserId = data.createdByUserId;
    return this.promptRequestRepo.create(createData);
  }

  // ── Approve (delete PromptRequest → create Prompt) ──

  async approve(requestId: string): Promise<Prompt> {
    const req = await this.getPromptRequest(requestId);

    // Create the approved prompt (carry priority from request)
    const createData: { name: string; content: string; projectId?: string; priority?: number } = {
      name: req.name,
      content: req.content,
    };
    if (req.projectId !== null) createData.projectId = req.projectId;
    if (req.priority !== 5) createData.priority = req.priority;

    const prompt = await this.promptRepo.create(createData);

    // Record the initial revision so the approved prompt has a v0.1.0 history entry.
    if (this.revisionService) {
      this.recordPromptRevision(prompt, '0.1.0', `approved promptrequest ${requestId}`).catch(() => {});
    }

    // Delete the request
    await this.promptRequestRepo.delete(requestId);

    return prompt;
  }

  // ── Backup/restore helpers ──

  async upsertByName(data: Record<string, unknown>): Promise<Prompt> {
    const name = data['name'] as string;
    let projectId: string | null = null;
    let agentId: string | null = null;

    if (data['project'] !== undefined) {
      const project = await this.projectRepo.findByName(data['project'] as string);
      if (project === null) throw new NotFoundError(`Project not found: ${data['project']}`);
      projectId = project.id;
    } else if (data['projectId'] !== undefined) {
      projectId = data['projectId'] as string;
    }

    if (data['agent'] !== undefined) {
      if (this.agentRepo === undefined) {
        throw new Error('Agent-scoped prompts require AgentRepository to be wired into PromptService');
      }
      const agent = await this.agentRepo.findByName(data['agent'] as string);
      if (agent === null) throw new NotFoundError(`Agent not found: ${data['agent']}`);
      agentId = agent.id;
    } else if (data['agentId'] !== undefined) {
      agentId = data['agentId'] as string;
    }

    if (projectId !== null && agentId !== null) {
      throw Object.assign(
        new Error('A prompt may attach to a project XOR an agent, not both'),
        { statusCode: 400 },
      );
    }

    const existing = agentId !== null
      ? await this.promptRepo.findByNameAndAgent(name, agentId)
      : await this.promptRepo.findByNameAndProject(name, projectId);

    if (existing !== null) {
      const updateData: { content?: string; priority?: number } = {};
      if (data['content'] !== undefined) updateData.content = data['content'] as string;
      if (data['priority'] !== undefined) updateData.priority = data['priority'] as number;
      if (Object.keys(updateData).length > 0) {
        return this.promptRepo.update(existing.id, updateData);
      }
      return existing;
    }

    const createData: { name: string; content: string; projectId?: string; agentId?: string; priority?: number; linkTarget?: string } = {
      name,
      content: (data['content'] as string) ?? '',
    };
    if (projectId !== null) createData.projectId = projectId;
    if (agentId !== null) createData.agentId = agentId;
    if (data['priority'] !== undefined) createData.priority = data['priority'] as number;
    if (data['linkTarget'] !== undefined) createData.linkTarget = data['linkTarget'] as string;

    return this.promptRepo.create(createData);
  }

  async deleteByName(name: string): Promise<void> {
    // Find first prompt with this name (across all projects)
    const all = await this.promptRepo.findAll();
    const match = all.find((p) => p.name === name);
    if (match === undefined) return;
    await this.promptRepo.delete(match.id);
  }

  // ── Visibility for MCP (approved prompts + session's pending requests) ──

  async getVisiblePrompts(
    projectId?: string,
    sessionId?: string,
  ): Promise<Array<{ name: string; content: string; priority: number; summary: string | null; chapters: string[] | null; linkTarget: string | null; type: 'prompt' | 'promptrequest' }>> {
    const results: Array<{ name: string; content: string; priority: number; summary: string | null; chapters: string[] | null; linkTarget: string | null; type: 'prompt' | 'promptrequest' }> = [];

    // Approved prompts (project-scoped + global)
    const prompts = await this.promptRepo.findAll(projectId);
    for (const p of prompts) {
      results.push({ name: p.name, content: p.content, priority: p.priority, summary: p.summary, chapters: p.chapters as string[] | null, linkTarget: p.linkTarget, type: 'prompt' });
    }

    // Session's own pending requests
    if (sessionId) {
      const requests = await this.promptRequestRepo.findBySession(sessionId, projectId);
      for (const r of requests) {
        results.push({ name: r.name, content: r.content, priority: 5, summary: null, chapters: null, linkTarget: null, type: 'promptrequest' });
      }
    }

    return results;
  }
}

