/**
 * AgentService — CRUD over `Agent` rows.
 *
 * Agents pin to one Llm (FK Restrict, so an Llm in active use can't be
 * deleted from under them) and optionally attach to a Project (FK SetNull —
 * agents survive project deletion). The service translates name-based
 * references (`{ llm: { name } }`, `{ project: { name } }`) to the FK ids
 * on write, and back to names on read so callers always work with stable
 * identifiers.
 */
import type { Agent } from '@prisma/client';
import type { IAgentRepository } from '../repositories/agent.repository.js';
import type { LlmService } from './llm.service.js';
import type { ProjectService } from './project.service.js';
import {
  CreateAgentSchema,
  UpdateAgentSchema,
  type AgentChatParams,
  type CreateAgentInput,
} from '../validation/agent.schema.js';
import { NotFoundError, ConflictError } from './mcp-server.service.js';

/** Shape returned by the API layer — embeds llm + project metadata. */
export interface AgentView {
  id: string;
  name: string;
  description: string;
  systemPrompt: string;
  llm: { id: string; name: string };
  project: { id: string; name: string } | null;
  proxyModelName: string | null;
  defaultParams: AgentChatParams;
  extras: Record<string, unknown>;
  ownerId: string;
  version: number;
  createdAt: Date;
  updatedAt: Date;
}

export class AgentService {
  constructor(
    private readonly repo: IAgentRepository,
    private readonly llms: LlmService,
    private readonly projects: ProjectService,
  ) {}

  async list(): Promise<AgentView[]> {
    const rows = await this.repo.findAll();
    return Promise.all(rows.map((r) => this.toView(r)));
  }

  async listByProject(projectName: string): Promise<AgentView[]> {
    const project = await this.projects.resolveAndGet(projectName);
    const rows = await this.repo.findByProjectId(project.id);
    return Promise.all(rows.map((r) => this.toView(r)));
  }

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

  async getByName(name: string): Promise<AgentView> {
    const row = await this.repo.findByName(name);
    if (row === null) throw new NotFoundError(`Agent not found: ${name}`);
    return this.toView(row);
  }

  async create(input: unknown, ownerId: string): Promise<AgentView> {
    const data = CreateAgentSchema.parse(input);
    const existing = await this.repo.findByName(data.name);
    if (existing !== null) throw new ConflictError(`Agent already exists: ${data.name}`);

    const llm = 'name' in data.llm ? await this.llms.getByName(data.llm.name) : await this.llms.getById(data.llm.id);
    const projectId = data.project !== undefined
      ? (await this.projects.resolveAndGet(data.project.name)).id
      : null;

    const row = await this.repo.create({
      name: data.name,
      description: data.description,
      systemPrompt: data.systemPrompt,
      llmId: llm.id,
      projectId,
      proxyModelName: data.proxyModelName ?? null,
      defaultParams: data.defaultParams as Record<string, unknown>,
      extras: data.extras,
      ownerId,
    });
    return this.toView(row);
  }

  async update(id: string, input: unknown): Promise<AgentView> {
    const data = UpdateAgentSchema.parse(input);
    await this.getById(id);

    const updateFields: Parameters<IAgentRepository['update']>[1] = {};
    if (data.description !== undefined) updateFields.description = data.description;
    if (data.systemPrompt !== undefined) updateFields.systemPrompt = data.systemPrompt;
    if (data.llm !== undefined) {
      const llm = 'name' in data.llm ? await this.llms.getByName(data.llm.name) : await this.llms.getById(data.llm.id);
      updateFields.llmId = llm.id;
    }
    if (data.project !== undefined) {
      updateFields.projectId = data.project === null
        ? null
        : (await this.projects.resolveAndGet(data.project.name)).id;
    }
    if (data.proxyModelName !== undefined) updateFields.proxyModelName = data.proxyModelName;
    if (data.defaultParams !== undefined) updateFields.defaultParams = data.defaultParams as Record<string, unknown>;
    if (data.extras !== undefined) updateFields.extras = data.extras;

    const row = await this.repo.update(id, updateFields);
    return this.toView(row);
  }

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

  // ── Backup/restore helpers ──

  async upsertByName(input: CreateAgentInput, ownerId: string): Promise<AgentView> {
    const existing = await this.repo.findByName(input.name);
    if (existing !== null) {
      return this.update(existing.id, input);
    }
    return this.create(input, ownerId);
  }

  async deleteByName(name: string): Promise<void> {
    const row = await this.repo.findByName(name);
    if (row === null) return;
    await this.delete(row.id);
  }

  private async toView(row: Agent): Promise<AgentView> {
    const llm = await this.llms.getById(row.llmId);
    const project = row.projectId !== null
      ? await this.projects.getById(row.projectId).catch(() => null)
      : null;
    return {
      id: row.id,
      name: row.name,
      description: row.description,
      systemPrompt: row.systemPrompt,
      llm: { id: llm.id, name: llm.name },
      project: project !== null ? { id: project.id, name: project.name } : null,
      proxyModelName: row.proxyModelName,
      defaultParams: row.defaultParams as AgentChatParams,
      extras: row.extras as Record<string, unknown>,
      ownerId: row.ownerId,
      version: row.version,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
    };
  }
}
