/**
 * Status-aware topology editor — wraps {@link useMazeApi} so the MazeNET
 * editor can call one set of primitives regardless of whether the
 * topology is ``pending`` (direct CRUD) or ``active|degraded`` (mutation
 * queue via :func:`enqueueMutation`).
 *
 * Primitives return a tagged {@link PrimitiveResult}:
 *   ``{ kind: 'applied', data }``   — backend wrote synchronously; the
 *                                     caller may update local state.
 *   ``{ kind: 'enqueued', mutationId }`` — mutator will apply async;
 *                                     caller must NOT touch local state,
 *                                     SSE ``mutation.applied`` drives refetch.
 *
 * Name arguments (``deckyName``, ``lanName``) are required on every
 * primitive because mutation ops are name-keyed while direct CRUD is
 * uuid-keyed. Callers plumb both.
 */
import { useMemo } from 'react';
import type {
  CreateDeckyBody,
  CreateLanBody,
  DeckyRow,
  EdgeRow,
  LANRow,
  MazeApi,
} from './useMazeApi';

export interface UseTopologyEditorOptions {
  api: MazeApi;
  /** Current topology status from :func:`getTopology`. */
  topoStatus: string;
  /** Last-known topology version for optimistic concurrency. */
  topoVersion: number;
}

export type PrimitiveResult<T> =
  | { kind: 'applied'; data: T }
  | { kind: 'enqueued'; mutationId: string };

export interface UseTopologyEditor {
  createLan(topologyId: string, body: CreateLanBody): Promise<PrimitiveResult<LANRow>>;
  updateLan(
    topologyId: string,
    lanId: string,
    lanName: string,
    patch: Partial<LANRow>,
  ): Promise<PrimitiveResult<LANRow>>;
  deleteLan(
    topologyId: string,
    lanId: string,
    lanName: string,
  ): Promise<PrimitiveResult<void>>;

  createDecky(topologyId: string, body: CreateDeckyBody): Promise<PrimitiveResult<DeckyRow>>;
  /** Composite: create a decky and attach it to its home LAN. On pending
   *  this is two CRUD calls; on active it's one ``add_decky`` enqueue.
   *  Callers should prefer this over ``createDecky`` + ``attachEdge`` so
   *  the active path doesn't 409 on the CRUD half. */
  addDeckyToLan(
    topologyId: string,
    body: CreateDeckyBody,
    lanId: string,
    lanName: string,
    opts?: { is_bridge?: boolean; forwards_l3?: boolean },
  ): Promise<PrimitiveResult<DeckyRow>>;
  updateDecky(
    topologyId: string,
    uuid: string,
    deckyName: string,
    patch: Partial<DeckyRow>,
    /** Extra top-level flags for the queued mutation payload — currently
     *  only ``force`` (opts in to destructive recreates like the
     *  forwards_l3 flip on a live topology).  Ignored on the pending
     *  CRUD path since pending edits never need force. */
    extras?: { force?: boolean },
  ): Promise<PrimitiveResult<DeckyRow>>;
  deleteDecky(
    topologyId: string,
    uuid: string,
    deckyName: string,
  ): Promise<PrimitiveResult<void>>;

  attachEdge(
    topologyId: string,
    body: { decky_uuid: string; lan_id: string; is_bridge?: boolean; forwards_l3?: boolean },
    deckyName: string,
    lanName: string,
  ): Promise<PrimitiveResult<EdgeRow>>;
  detachEdge(
    topologyId: string,
    edgeId: string,
    deckyName: string,
    lanName: string,
  ): Promise<PrimitiveResult<void>>;
}

export function useTopologyEditor(
  opts: UseTopologyEditorOptions,
): UseTopologyEditor {
  const { api, topoStatus, topoVersion } = opts;
  const live = topoStatus === 'active' || topoStatus === 'degraded';

  return useMemo<UseTopologyEditor>(() => ({
    // ── LAN ────────────────────────────────────────────────────────────
    async createLan(topologyId, body) {
      if (!live) {
        const data = await api.createLan(topologyId, body);
        return { kind: 'applied', data };
      }
      // add_lan payload: {name, subnet?, is_dmz?, x?, y?}
      const payload: Record<string, unknown> = { name: body.name };
      if (body.subnet !== undefined) payload.subnet = body.subnet;
      if (body.is_dmz !== undefined) payload.is_dmz = body.is_dmz;
      if (body.x !== undefined) payload.x = body.x;
      if (body.y !== undefined) payload.y = body.y;
      const res = await api.enqueueMutation(topologyId, 'add_lan', payload, topoVersion);
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
    async updateLan(topologyId, lanId, lanName, patch) {
      if (!live) {
        const data = await api.updateLan(topologyId, lanId, patch);
        return { kind: 'applied', data };
      }
      const payload: Record<string, unknown> = { name: lanName };
      const patchFields: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(patch)) {
        if (k === 'x' || k === 'y') payload[k] = v;
        else patchFields[k] = v;
      }
      if (Object.keys(patchFields).length > 0) payload.patch = patchFields;
      const res = await api.enqueueMutation(topologyId, 'update_lan', payload, topoVersion);
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
    async deleteLan(topologyId, lanId, lanName) {
      if (!live) {
        await api.deleteLan(topologyId, lanId);
        return { kind: 'applied', data: undefined };
      }
      const res = await api.enqueueMutation(
        topologyId, 'remove_lan', { name: lanName }, topoVersion,
      );
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },

    // ── Decky ──────────────────────────────────────────────────────────
    async createDecky(topologyId, body) {
      // Bare create — only valid on pending. On active callers should use
      // addDeckyToLan() instead; the backend guard will 409 here.
      const data = await api.createDecky(topologyId, body);
      return { kind: 'applied', data };
    },
    async addDeckyToLan(topologyId, body, lanId, lanName, opts) {
      if (!live) {
        const data = await api.createDecky(topologyId, body);
        await api.attachEdge(topologyId, {
          decky_uuid: data.uuid,
          lan_id: lanId,
          is_bridge: opts?.is_bridge,
          forwards_l3: opts?.forwards_l3,
        });
        return { kind: 'applied', data };
      }
      const payload: Record<string, unknown> = {
        name: body.name,
        lan: lanName,
        services: body.services,
      };
      const cfg = body.decky_config ?? {};
      if (cfg.archetype !== undefined) payload.archetype = cfg.archetype;
      const fwd = opts?.forwards_l3 ?? cfg.forwards_l3;
      if (fwd !== undefined) payload.forwards_l3 = fwd;
      if (body.x !== undefined) payload.x = body.x;
      if (body.y !== undefined) payload.y = body.y;
      const res = await api.enqueueMutation(topologyId, 'add_decky', payload, topoVersion);
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
    async updateDecky(topologyId, uuid, deckyName, patch, extras) {
      if (!live) {
        const data = await api.updateDecky(topologyId, uuid, patch);
        return { kind: 'applied', data };
      }
      const payload: Record<string, unknown> = { decky: deckyName };
      const patchFields: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(patch)) {
        if (k === 'services' || k === 'x' || k === 'y') payload[k] = v;
        else patchFields[k] = v;
      }
      if (Object.keys(patchFields).length > 0) payload.patch = patchFields;
      if (extras?.force) payload.force = true;
      const res = await api.enqueueMutation(topologyId, 'update_decky', payload, topoVersion);
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
    async deleteDecky(topologyId, uuid, deckyName) {
      if (!live) {
        await api.deleteDecky(topologyId, uuid);
        return { kind: 'applied', data: undefined };
      }
      const res = await api.enqueueMutation(
        topologyId, 'remove_decky', { decky: deckyName }, topoVersion,
      );
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },

    // ── Edges ──────────────────────────────────────────────────────────
    async attachEdge(topologyId, body, deckyName, lanName) {
      if (!live) {
        const data = await api.attachEdge(topologyId, body);
        return { kind: 'applied', data };
      }
      const payload: Record<string, unknown> = { decky: deckyName, lan: lanName };
      if (body.forwards_l3 !== undefined) payload.forwards_l3 = body.forwards_l3;
      const res = await api.enqueueMutation(topologyId, 'attach_decky', payload, topoVersion);
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
    async detachEdge(topologyId, edgeId, deckyName, lanName) {
      if (!live) {
        await api.detachEdge(topologyId, edgeId);
        return { kind: 'applied', data: undefined };
      }
      const res = await api.enqueueMutation(
        topologyId, 'detach_decky', { decky: deckyName, lan: lanName }, topoVersion,
      );
      return { kind: 'enqueued', mutationId: res.mutation_id };
    },
  }), [api, live, topoVersion]);
}
