import crypto from 'node:crypto';
import type { Prisma, ResourceRevision } from '@prisma/client';

import type {
  IResourceRevisionRepository,
  ResourceType,
} from '../repositories/resource-revision.repository.js';

export interface RecordRevisionInput {
  resourceType: ResourceType;
  resourceId: string;
  /** New semver — caller computes via bumpSemver / explicit override. */
  semver: string;
  /**
   * Snapshot of the resource body at this revision. Shape is
   * resource-specific — for Prompt: `{ content, priority, linkTarget }`;
   * for Skill: `{ content, files, metadata, priority, description }`.
   * Stored as-is in `body` (jsonb) and used as the diff/restore source
   * by the revisions API.
   */
  body: Record<string, unknown>;
  authorUserId?: string;
  authorSessionId?: string;
  note?: string;
}

export class ResourceRevisionService {
  constructor(private readonly repo: IResourceRevisionRepository) {}

  /**
   * sha256 of the canonicalised body. Stable across key reorderings so a
   * resource that's saved twice with the same logical content produces
   * the same hash on both revisions — useful for sync-side dedup.
   */
  static hash(body: unknown): string {
    return 'sha256:' + crypto.createHash('sha256').update(canonicalJson(body)).digest('hex');
  }

  async record(
    input: RecordRevisionInput,
    tx?: Prisma.TransactionClient,
  ): Promise<{ revision: ResourceRevision; contentHash: string }> {
    const contentHash = ResourceRevisionService.hash(input.body);
    const revision = await this.repo.create(
      {
        resourceType: input.resourceType,
        resourceId: input.resourceId,
        semver: input.semver,
        contentHash,
        body: input.body as Prisma.InputJsonValue,
        ...(input.authorUserId !== undefined ? { authorUserId: input.authorUserId } : {}),
        ...(input.authorSessionId !== undefined ? { authorSessionId: input.authorSessionId } : {}),
        ...(input.note !== undefined ? { note: input.note } : {}),
      },
      tx,
    );
    return { revision, contentHash };
  }

  async getById(id: string): Promise<ResourceRevision | null> {
    return this.repo.findById(id);
  }

  async listHistory(
    resourceType: ResourceType,
    resourceId: string,
    limit?: number,
  ): Promise<ResourceRevision[]> {
    return this.repo.findHistory(resourceType, resourceId, limit);
  }

  async findBySemver(
    resourceType: ResourceType,
    resourceId: string,
    semver: string,
  ): Promise<ResourceRevision | null> {
    return this.repo.findBySemver(resourceType, resourceId, semver);
  }
}

/**
 * Canonical JSON: keys sorted at every object level. Used by `hash` so
 * `{a:1,b:2}` and `{b:2,a:1}` produce the same digest.
 */
function canonicalJson(v: unknown): string {
  if (v === null || v === undefined || typeof v !== 'object') {
    return JSON.stringify(v ?? null);
  }
  if (Array.isArray(v)) {
    return '[' + v.map(canonicalJson).join(',') + ']';
  }
  const obj = v as Record<string, unknown>;
  const keys = Object.keys(obj).sort();
  return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(obj[k])).join(',') + '}';
}
