// lib/tools/adapters/workflow-definitions.js
//
// Adapters for `create_workflow_definition` and `update_workflow_definition`.
// Both call upsertDefinition() directly — the same single write path used by
// the admin JSON editor route. The strict spec-validator (lib/process/
// spec-validator.js) is the authority on shape; approval/allowlist/rate-limit
// gates fire in executeRegistryTool BEFORE these adapters run.
//
// Provenance: options.agent_id and options.acting_on_behalf_of_user_id are
// propagated into the created_by_* columns on process_definitions.
//
// Why direct call (not HTTP fetch via inproc token):
//   - executeRegistryTool already runs all gates (approval, allowlist,
//     rate-limit). Going through HTTP adds no security — it just couples this
//     adapter to the dev-server lifecycle, port discovery, and inproc-token
//     plumbing. The single write path invariant lives in upsertDefinition,
//     which both surfaces call.
//   - upsertDefinition is the SOLE writer of process_definitions per the
//     engine invariant in ai-docs/process-engine.md.

import { getDb } from '../../db.js';
import { getDefinition, upsertDefinition } from '../../process/definitions.js';

function asActingUserId(options) {
  return options?.acting_on_behalf_of_user_id || options?.user_id || null;
}

function hasWorkflowAuthoringAuthority(options = {}) {
  if (options.owner_trusted) return true;
  if (options.actor_role === 'owner' || options.actor_role === 'admin') return true;
  const userId = asActingUserId(options);
  if (!userId || userId === 'system') return false;
  try {
    const row = getDb()
      .prepare("SELECT role FROM org_memberships WHERE user_id=? AND org_id=COALESCE(?, 'personal-org')")
      .get(userId, options.org_id || 'personal-org');
    return row?.role === 'owner' || row?.role === 'admin';
  } catch {
    return false;
  }
}

function insufficientRole() {
  return {
    ok: false,
    status: 'error',
    error: 'insufficient_role',
    message: 'Workflow definition authoring requires owner/admin authority in addition to tool approval.',
  };
}

function structuredErrorFromUpsert(err) {
  if (err?.code === 'spec_invalid') {
    return { ok: false, status: 'error', error: 'spec_invalid', errors: err.errors || [err.message] };
  }
  if (err?.code === 'version_bump_required') {
    return { ok: false, status: 'error', error: 'version_bump_required', message: err.message };
  }
  return null;
}

/**
 * Create a new workflow definition from a JSON spec.
 *
 * Refuses to overwrite an existing slug — that path is `update_workflow_definition`.
 * Returns { ok: true, definition_id, slug, version } on success, or
 * { ok: false, error, ... } on validation failure or slug conflict.
 */
export async function createWorkflowDefinition(input, options = {}) {
  if (!hasWorkflowAuthoringAuthority(options)) return insufficientRole();

  const spec = input?.spec;
  if (!spec || typeof spec !== 'object') {
    return { ok: false, status: 'error', error: 'spec_required', message: 'spec required (object)' };
  }
  if (!spec.slug || typeof spec.slug !== 'string') {
    return { ok: false, error: 'slug_required', message: 'spec.slug required (string)' };
  }

  const existing = getDefinition(spec.slug);
  if (existing && existing.status !== 'archived') {
    return { ok: false, status: 'error', error: 'slug_in_use', slug: spec.slug };
  }

  try {
    const def = upsertDefinition(spec, {
      source_spec_json: JSON.stringify(spec),
      created_by_user_id: asActingUserId(options),
      created_by_agent_id: options?.agent_id || null,
    });
    return {
      ok: true,
      status: 'complete',
      definition_id: def.id,
      slug: def.slug,
      version: def.version,
    };
  } catch (err) {
    const structured = structuredErrorFromUpsert(err);
    if (structured) return structured;
    throw err;
  }
}

/**
 * Update an existing workflow definition. The slug is taken from input.slug
 * (path-style) and forced onto spec.slug so an agent cannot rename out from
 * under itself. version must be bumped if the stage list is structurally
 * different — upsertDefinition enforces that and returns
 * { error: 'version_bump_required' } if not.
 */
export async function updateWorkflowDefinition(input, options = {}) {
  if (!hasWorkflowAuthoringAuthority(options)) return insufficientRole();

  const slug = typeof input?.slug === 'string' ? input.slug.trim() : '';
  const spec = input?.spec;
  if (!slug) {
    return { ok: false, status: 'error', error: 'slug_required', message: 'slug required (string)' };
  }
  if (!spec || typeof spec !== 'object') {
    return { ok: false, status: 'error', error: 'spec_required', message: 'spec required (object)' };
  }

  const existing = getDefinition(slug);
  if (!existing) {
    return { ok: false, status: 'error', error: 'not_found', slug };
  }

  // Force the slug to match — defensive copy so we never mutate the caller's
  // spec object (the API route does the same thing).
  const cloned = { ...spec, slug };

  try {
    const def = upsertDefinition(cloned, {
      source_spec_json: JSON.stringify(cloned),
      created_by_user_id: asActingUserId(options),
      created_by_agent_id: options?.agent_id || null,
    });
    return {
      ok: true,
      status: 'complete',
      definition_id: def.id,
      slug: def.slug,
      version: def.version,
    };
  } catch (err) {
    const structured = structuredErrorFromUpsert(err);
    if (structured) return structured;
    throw err;
  }
}
