/**
 * PersonalityService — CRUD over `Personality` rows + prompt bindings.
 *
 * A Personality is a named overlay attached to one Agent. At chat time, the
 * caller may pick a Personality (by `--personality` flag, or by the agent's
 * `defaultPersonalityId`); the prompts bound to that Personality are appended
 * to the system block on top of the agent's own systemPrompt + agent-direct
 * prompts + project prompts. VLAN-on-ethernet: the agent works without one;
 * with one, segmentation kicks in.
 *
 * Scope rule for attaching a Prompt to a Personality (enforced here, not in
 * the DB, so error messages stay readable):
 *   A prompt may be bound only if it's:
 *     - directly attached to the same Agent (Prompt.agentId === personality.agentId), OR
 *     - in the Agent's Project (Prompt.projectId === agent.projectId, when projectId set), OR
 *     - global (Prompt.projectId === null && Prompt.agentId === null).
 *   Attaching a prompt from a foreign project / foreign agent is rejected.
 */
import type { Personality, PersonalityPrompt, Prompt } from '@prisma/client';
import type { IPersonalityRepository } from '../repositories/personality.repository.js';
import type { IAgentRepository } from '../repositories/agent.repository.js';
import type { IPromptRepository } from '../repositories/prompt.repository.js';
import {
  CreatePersonalitySchema,
  UpdatePersonalitySchema,
  AttachPromptSchema,
} from '../validation/personality.schema.js';
import { NotFoundError, ConflictError } from './mcp-server.service.js';

export interface PersonalityView {
  id: string;
  name: string;
  description: string;
  agentId: string;
  agentName: string;
  priority: number;
  promptCount: number;
  createdAt: Date;
  updatedAt: Date;
}

export interface PersonalityPromptView {
  promptId: string;
  promptName: string;
  promptContent: string;
  priority: number; // PersonalityPrompt-scoped priority (overrides Prompt.priority within this overlay)
  createdAt: Date;
}

export class PersonalityService {
  constructor(
    private readonly repo: IPersonalityRepository,
    private readonly agentRepo: IAgentRepository,
    private readonly promptRepo: IPromptRepository,
  ) {}

  async listAll(): Promise<PersonalityView[]> {
    const rows = await this.repo.findAll();
    const agents = new Map<string, string>();
    for (const r of rows) {
      if (!agents.has(r.agentId)) {
        const agent = await this.agentRepo.findById(r.agentId);
        agents.set(r.agentId, agent?.name ?? r.agentId);
      }
    }
    return Promise.all(rows.map((r) => this.toView(r, agents.get(r.agentId) ?? r.agentId)));
  }

  async listForAgent(agentName: string): Promise<PersonalityView[]> {
    const agent = await this.agentRepo.findByName(agentName);
    if (agent === null) throw new NotFoundError(`Agent not found: ${agentName}`);
    const rows = await this.repo.findByAgent(agent.id);
    return Promise.all(rows.map((r) => this.toView(r, agent.name)));
  }

  async getById(id: string): Promise<PersonalityView> {
    const row = await this.repo.findById(id);
    if (row === null) throw new NotFoundError(`Personality not found: ${id}`);
    const agent = await this.agentRepo.findById(row.agentId);
    return this.toView(row, agent?.name ?? row.agentId);
  }

  async create(agentName: string, input: unknown): Promise<PersonalityView> {
    const agent = await this.agentRepo.findByName(agentName);
    if (agent === null) throw new NotFoundError(`Agent not found: ${agentName}`);

    // Inject the resolved agentId so the schema can validate the full shape
    // (callers pass `{ name, description?, priority? }` without re-stating it).
    const data = CreatePersonalitySchema.parse({
      ...(input as Record<string, unknown>),
      agentId: agent.id,
    });

    const existing = await this.repo.findByNameAndAgent(data.name, agent.id);
    if (existing !== null) {
      throw new ConflictError(`Personality already exists: ${agentName}/${data.name}`);
    }

    const created = await this.repo.create({
      name: data.name,
      description: data.description,
      agentId: agent.id,
      priority: data.priority,
    });
    return this.toView(created, agent.name);
  }

  async update(id: string, input: unknown): Promise<PersonalityView> {
    const data = UpdatePersonalitySchema.parse(input);
    const existing = await this.repo.findById(id);
    if (existing === null) throw new NotFoundError(`Personality not found: ${id}`);

    const updateFields: Parameters<IPersonalityRepository['update']>[1] = {};
    if (data.description !== undefined) updateFields.description = data.description;
    if (data.priority !== undefined) updateFields.priority = data.priority;

    const updated = await this.repo.update(id, updateFields);
    const agent = await this.agentRepo.findById(updated.agentId);
    return this.toView(updated, agent?.name ?? updated.agentId);
  }

  async delete(id: string): Promise<void> {
    const existing = await this.repo.findById(id);
    if (existing === null) throw new NotFoundError(`Personality not found: ${id}`);
    await this.repo.delete(id);
  }

  // ── Prompt bindings ──

  async listBoundPrompts(personalityId: string): Promise<PersonalityPromptView[]> {
    const personality = await this.repo.findById(personalityId);
    if (personality === null) throw new NotFoundError(`Personality not found: ${personalityId}`);

    const rows = await this.repo.listPrompts(personalityId);
    return rows.map((r) => ({
      promptId: r.prompt.id,
      promptName: r.prompt.name,
      promptContent: r.prompt.content,
      priority: r.priority,
      createdAt: r.createdAt,
    }));
  }

  async attachPrompt(personalityId: string, input: unknown): Promise<PersonalityPrompt> {
    const data = AttachPromptSchema.parse(input);
    const personality = await this.repo.findById(personalityId);
    if (personality === null) throw new NotFoundError(`Personality not found: ${personalityId}`);

    const prompt = await this.promptRepo.findById(data.promptId);
    if (prompt === null) throw new NotFoundError(`Prompt not found: ${data.promptId}`);

    await this.assertPromptInScope(prompt, personality);

    const dup = await this.repo.findBinding(personalityId, data.promptId);
    if (dup !== null) {
      throw new ConflictError(`Prompt already bound to personality: ${prompt.name}`);
    }

    return this.repo.attachPrompt(personalityId, data.promptId, data.priority);
  }

  async detachPrompt(personalityId: string, promptId: string): Promise<void> {
    const personality = await this.repo.findById(personalityId);
    if (personality === null) throw new NotFoundError(`Personality not found: ${personalityId}`);
    const binding = await this.repo.findBinding(personalityId, promptId);
    if (binding === null) {
      throw new NotFoundError(`Prompt not bound to personality: ${promptId}`);
    }
    await this.repo.detachPrompt(personalityId, promptId);
  }

  /**
   * A prompt may overlay a personality only if it shares scope with the
   * agent — direct attachment, the agent's project, or global. Anything else
   * is a leak attempt (e.g., someone trying to attach Project A's prompts to
   * Project B's agent's personality).
   */
  private async assertPromptInScope(prompt: Prompt, personality: Personality): Promise<void> {
    if (prompt.agentId !== null) {
      if (prompt.agentId !== personality.agentId) {
        throw Object.assign(
          new Error(`Prompt belongs to a different agent and cannot be attached to this personality`),
          { statusCode: 400 },
        );
      }
      return;
    }
    if (prompt.projectId !== null) {
      const agent = await this.agentRepo.findById(personality.agentId);
      if (agent === null) {
        throw new NotFoundError(`Agent not found: ${personality.agentId}`);
      }
      if (agent.projectId !== prompt.projectId) {
        throw Object.assign(
          new Error(`Prompt belongs to a different project and cannot be attached to this personality`),
          { statusCode: 400 },
        );
      }
      return;
    }
    // Global prompt — allowed for any personality.
  }

  private async toView(row: Personality, agentName: string): Promise<PersonalityView> {
    const prompts = await this.repo.listPrompts(row.id);
    return {
      id: row.id,
      name: row.name,
      description: row.description,
      agentId: row.agentId,
      agentName,
      priority: row.priority,
      promptCount: prompts.length,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
    };
  }
}
