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

import type { ISkillRepository } from '../repositories/skill.repository.js';
import type { IProjectRepository } from '../repositories/project.repository.js';
import type { IAgentRepository } from '../repositories/agent.repository.js';
import { CreateSkillSchema, UpdateSkillSchema } from '../validation/skill.schema.js';
import { NotFoundError } from './mcp-server.service.js';
import { ResourceRevisionService } from './resource-revision.service.js';
import type { ResourceProposalService } from './resource-proposal.service.js';
import { bumpSemver, type BumpKind } from '../utils/semver.js';

export class SkillService {
  private revisionService: ResourceRevisionService | null = null;

  constructor(
    private readonly skillRepo: ISkillRepository,
    private readonly projectRepo: IProjectRepository,
    private readonly agentRepo?: IAgentRepository,
  ) {}

  setRevisionService(service: ResourceRevisionService): void {
    this.revisionService = service;
  }

  /**
   * Register a 'skill' approval handler with the proposal service. Mirrors
   * PromptService's setup: approve = upsert skill body + record revision +
   * link currentRevisionId, all inside a single transaction.
   */
  setProposalService(service: ResourceProposalService): void {
    service.setHandler('skill', async (proposal, tx, _approverUserId) => {
      const body = (proposal.body ?? {}) as Record<string, unknown>;
      const content = String(body['content'] ?? '');
      const description = typeof body['description'] === 'string' ? body['description'] : '';
      const priority = typeof body['priority'] === 'number' ? body['priority'] : 5;
      const files = (body['files'] ?? {}) as Prisma.InputJsonValue;
      const metadata = (body['metadata'] ?? {}) as Prisma.InputJsonValue;
      const projectId = proposal.projectId ?? null;
      const agentId = proposal.agentId ?? null;

      const existing = agentId !== null
        ? await tx.skill.findUnique({ where: { name_agentId: { name: proposal.name, agentId } } })
        : await tx.skill.findUnique({ where: { name_projectId: { name: proposal.name, projectId: projectId ?? '' } } });

      let skillId: string;
      let newSemver: string;
      if (existing !== null) {
        newSemver = bumpSemver(existing.semver, 'patch');
        await tx.skill.update({
          where: { id: existing.id },
          data: { content, description, priority, files, metadata, semver: newSemver },
        });
        skillId = existing.id;
      } else {
        newSemver = '0.1.0';
        const created = await tx.skill.create({
          data: {
            name: proposal.name,
            content,
            description,
            priority,
            files,
            metadata,
            ...(projectId !== null ? { projectId } : {}),
            ...(agentId !== null ? { agentId } : {}),
            semver: newSemver,
          },
        });
        skillId = created.id;
      }

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

      await tx.skill.update({
        where: { id: skillId },
        data: { currentRevisionId: revision.id },
      });

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

  // ── CRUD ──

  async listSkills(projectId?: string): Promise<Skill[]> {
    return this.skillRepo.findAll(projectId);
  }

  async listGlobalSkills(): Promise<Skill[]> {
    return this.skillRepo.findGlobal();
  }

  async listSkillsForAgent(agentId: string): Promise<Skill[]> {
    return this.skillRepo.findByAgent(agentId);
  }

  async getSkill(id: string): Promise<Skill> {
    const skill = await this.skillRepo.findById(id);
    if (skill === null) throw new NotFoundError(`Skill not found: ${id}`);
    return skill;
  }

  async createSkill(input: unknown): Promise<Skill> {
    const data = CreateSkillSchema.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 skills require AgentRepository to be wired into SkillService');
      }
      const agent = await this.agentRepo.findById(data.agentId);
      if (agent === null) throw new NotFoundError(`Agent not found: ${data.agentId}`);
    }

    const createData: {
      name: string;
      content: string;
      description?: string;
      files?: Prisma.InputJsonValue;
      metadata?: Prisma.InputJsonValue;
      projectId?: string;
      agentId?: string;
      priority?: number;
      semver?: string;
    } = {
      name: data.name,
      content: data.content,
    };
    if (data.description !== undefined) createData.description = data.description;
    if (data.files !== undefined) createData.files = data.files as Prisma.InputJsonValue;
    if (data.metadata !== undefined) createData.metadata = data.metadata as Prisma.InputJsonValue;
    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.semver !== undefined) createData.semver = data.semver;

    const skill = await this.skillRepo.create(createData);

    if (this.revisionService) {
      this.recordSkillRevision(skill, skill.semver, 'created').catch(() => {});
    }
    return skill;
  }

  async updateSkill(id: string, input: unknown): Promise<Skill> {
    const data = UpdateSkillSchema.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.getSkill(id);

    let newSemver = existing.semver;
    const contentOrMetaChanged =
      data.content !== undefined ||
      data.description !== undefined ||
      data.files !== undefined ||
      data.metadata !== undefined ||
      data.priority !== undefined;

    if (data.semver !== undefined) {
      newSemver = data.semver;
    } else if (data.bump !== undefined) {
      newSemver = bumpSemver(existing.semver, data.bump as BumpKind);
    } else if (contentOrMetaChanged) {
      newSemver = bumpSemver(existing.semver, 'patch');
    }

    const updateData: {
      content?: string;
      description?: string;
      files?: Prisma.InputJsonValue;
      metadata?: Prisma.InputJsonValue;
      priority?: number;
      semver?: string;
    } = {};
    if (data.content !== undefined) updateData.content = data.content;
    if (data.description !== undefined) updateData.description = data.description;
    if (data.files !== undefined) updateData.files = data.files as Prisma.InputJsonValue;
    if (data.metadata !== undefined) updateData.metadata = data.metadata as Prisma.InputJsonValue;
    if (data.priority !== undefined) updateData.priority = data.priority;
    if (newSemver !== existing.semver) updateData.semver = newSemver;

    const skill = await this.skillRepo.update(id, updateData);

    const shouldRecord =
      contentOrMetaChanged || data.bump !== undefined || data.semver !== undefined;
    if (this.revisionService && shouldRecord) {
      this.recordSkillRevision(skill, newSemver, data.note ?? null).catch(() => {});
    }
    return skill;
  }

  /** Best-effort revision write — same shape as PromptService. */
  private async recordSkillRevision(skill: Skill, semver: string, note: string | null): Promise<void> {
    if (this.revisionService === null) return;
    const body: Record<string, unknown> = {
      content: skill.content,
      description: skill.description,
      priority: skill.priority,
      files: skill.files,
      metadata: skill.metadata,
    };
    const { revision } = await this.revisionService.record({
      resourceType: 'skill',
      resourceId: skill.id,
      semver,
      body,
      ...(note !== null ? { note } : {}),
    });
    await this.skillRepo.update(skill.id, { currentRevisionId: revision.id });
  }

  async restoreRevisionForSkill(skillId: string, revisionId: string, note?: string): Promise<Skill> {
    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 !== 'skill' || revision.resourceId !== skillId) {
      throw Object.assign(
        new Error('Revision does not belong to this skill'),
        { statusCode: 400 },
      );
    }
    const body = (revision.body ?? {}) as Record<string, unknown>;
    return this.updateSkill(skillId, {
      content: typeof body['content'] === 'string' ? body['content'] : undefined,
      description: typeof body['description'] === 'string' ? body['description'] : undefined,
      priority: typeof body['priority'] === 'number' ? body['priority'] : undefined,
      files: body['files'] as Record<string, string> | undefined,
      metadata: body['metadata'] as Record<string, unknown> | undefined,
      bump: 'patch',
      note: note ?? `restored from revision ${revisionId}`,
    });
  }

  async deleteSkill(id: string): Promise<void> {
    await this.getSkill(id); // 404 if missing
    await this.skillRepo.delete(id);
  }

  // ── Backup/restore helpers ──

  async upsertByName(data: Record<string, unknown>): Promise<Skill> {
    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 skills require AgentRepository to be wired into SkillService');
      }
      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 skill may attach to a project XOR an agent, not both'),
        { statusCode: 400 },
      );
    }

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

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

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

    return this.skillRepo.create(createData);
  }

  async deleteByName(name: string): Promise<void> {
    const all = await this.skillRepo.findAll();
    const match = all.find((s) => s.name === name);
    if (match === undefined) return;
    await this.skillRepo.delete(match.id);
  }

  /**
   * Visibility for `mcpctl skills sync` (PR-5). Returns metadata only —
   * no `files` or full `content` — so the diff path can quickly decide
   * what's stale via contentHash + semver before fetching bodies.
   */
  async getVisibleSkills(projectId?: string): Promise<Array<{
    id: string;
    name: string;
    description: string;
    semver: string;
    contentHash: string;
    metadata: unknown;
    scope: 'project' | 'global' | 'agent';
  }>> {
    const skills = await this.skillRepo.findAll(projectId);
    const out: Array<{
      id: string;
      name: string;
      description: string;
      semver: string;
      contentHash: string;
      metadata: unknown;
      scope: 'project' | 'global' | 'agent';
    }> = [];
    for (const s of skills) {
      let scope: 'project' | 'global' | 'agent' = 'global';
      if (s.projectId !== null) scope = 'project';
      else if (s.agentId !== null) scope = 'agent';
      // Compute contentHash on the fly from the body shape that
      // `mcpctl skills sync` will write to disk. The server hashes the
      // canonicalised JSON; the client hashes the same JSON shape it
      // receives, and they match. (Cheap — sha256 of a few KB.)
      const contentHash = ResourceRevisionService.hash({
        content: s.content,
        description: s.description,
        priority: s.priority,
        files: s.files,
        metadata: s.metadata,
      });
      out.push({
        id: s.id,
        name: s.name,
        description: s.description,
        semver: s.semver,
        contentHash,
        metadata: s.metadata,
        scope,
      });
    }
    return out;
  }
}
