/**
 * The mcpctl Pulumi dynamic provider.
 *
 * A dynamic provider's object is *serialized* into the deployment. This package
 * compiles to CommonJS, so these top-level imports emit as `require(...)` in the
 * built output; Pulumi's closure serializer captures those module references
 * (the helpers are pure and dependency-light) and re-requires them at runtime.
 * The `@pulumi/pulumi` import is type-only, so it is erased and leaves no
 * runtime reference in the serialized object.
 *
 * CRUD maps onto mcpd's REST API via ./engine, mirroring the CLI's `apply`
 * (upsert) and `get -o json` (round-trip) contracts. Validation is deferred to
 * mcpd's Zod schemas — this provider only enforces the few client-side
 * invariants (name shape, required connection).
 */
import type * as pulumi from '@pulumi/pulumi';
import { upsert, findByName, del } from './engine';
import {
  resourceForKind,
  immutableFieldsForKind,
  normalizeSpec,
  stripForRead,
  KIND_IMMUTABLE_FIELDS,
} from './kinds';

/** Inputs for a generic mcpctl resource (all values resolved by the time the
 * provider sees them). */
export interface McpctlResourceInputs {
  kind: string;
  name: string;
  spec: Record<string, unknown>;
  mcpd: { url: string; token: string; timeoutMs?: number };
}

/** Deterministic JSON for order-insensitive equality checks. */
function stableStringify(value: unknown): string {
  if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  const obj = value as Record<string, unknown>;
  const keys = Object.keys(obj).sort();
  return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
}

export const mcpctlProvider: pulumi.dynamic.ResourceProvider = {
  async check(_olds: unknown, news: unknown): Promise<pulumi.dynamic.CheckResult> {
    const input = (news ?? {}) as Partial<McpctlResourceInputs>;
    const failures: pulumi.dynamic.CheckFailure[] = [];

    if (typeof input.kind !== 'string' || input.kind.length === 0) {
      failures.push({ property: 'kind', reason: 'kind is required (e.g. "llm", "server", "project")' });
    }
    if (typeof input.name !== 'string' || !/^[a-z0-9-]+$/.test(input.name)) {
      failures.push({ property: 'name', reason: 'name is required and must be lowercase alphanumeric with hyphens' });
    }
    if (input.spec === null || typeof input.spec !== 'object' || Array.isArray(input.spec)) {
      failures.push({ property: 'spec', reason: 'spec must be an object' });
    }
    const mcpd = input.mcpd;
    if (mcpd === undefined || typeof mcpd.url !== 'string' || mcpd.url.length === 0) {
      failures.push({ property: 'mcpd', reason: 'mcpd.url is required' });
    }
    if (mcpd === undefined || typeof mcpd.token !== 'string' || mcpd.token.length === 0) {
      failures.push({ property: 'mcpd', reason: 'mcpd.token is required' });
    }
    if (failures.length > 0) return { failures };

    const kind = input.kind as string;
    const spec = input.spec as Record<string, unknown>;
    return { inputs: { ...input, spec: normalizeSpec(kind, spec) } };
  },

  async diff(_id: string, olds: unknown, news: unknown): Promise<pulumi.dynamic.DiffResult> {
    const o = olds as McpctlResourceInputs;
    const n = news as McpctlResourceInputs;
    const replaces: string[] = [];

    if (o.kind !== n.kind) replaces.push('kind');
    if (o.name !== n.name) replaces.push('name');
    // Re-pointing at a different daemon is a different resource.
    if (o.mcpd?.url !== n.mcpd?.url) replaces.push('mcpd');

    const immutable = KIND_IMMUTABLE_FIELDS[n.kind] ?? [];
    for (const field of immutable) {
      if (stableStringify(o.spec?.[field]) !== stableStringify(n.spec?.[field])) {
        replaces.push(`spec.${field}`);
      }
    }

    const specChanged = stableStringify(o.spec ?? {}) !== stableStringify(n.spec ?? {});
    const changes = replaces.length > 0 || specChanged;
    // Any replacement keeps or reuses the unique `name`, so the old row must go
    // first to avoid a uniqueness collision (or an orphan on rename).
    return { changes, replaces, deleteBeforeReplace: replaces.length > 0 };
  },

  async create(inputs: unknown): Promise<pulumi.dynamic.CreateResult> {
    const inp = inputs as McpctlResourceInputs;
    const spec = normalizeSpec(inp.kind, inp.spec ?? {});
    const entity = await upsert(inp.mcpd, resourceForKind(inp.kind), { name: inp.name, ...spec }, immutableFieldsForKind(inp.kind));
    return { id: `${inp.kind}/${inp.name}`, outs: { kind: inp.kind, name: inp.name, spec, mcpd: inp.mcpd, resourceId: entity.id } };
  },

  async read(id: string, props: unknown): Promise<pulumi.dynamic.ReadResult> {
    const p = props as McpctlResourceInputs;
    const entity = await findByName(p.mcpd, resourceForKind(p.kind), p.name);
    if (!entity) return {}; // gone — Pulumi will recreate
    const spec = stripForRead(p.kind, entity);
    delete spec['name'];
    return { id, props: { kind: p.kind, name: p.name, mcpd: p.mcpd, spec, resourceId: entity.id } };
  },

  async update(_id: string, _olds: unknown, news: unknown): Promise<pulumi.dynamic.UpdateResult> {
    const n = news as McpctlResourceInputs;
    const spec = normalizeSpec(n.kind, n.spec ?? {});
    const entity = await upsert(n.mcpd, resourceForKind(n.kind), { name: n.name, ...spec }, immutableFieldsForKind(n.kind));
    return { outs: { kind: n.kind, name: n.name, spec, mcpd: n.mcpd, resourceId: entity.id } };
  },

  async delete(_id: string, props: unknown): Promise<void> {
    const p = props as McpctlResourceInputs;
    await del(p.mcpd, resourceForKind(p.kind), p.name);
  },
};
