/**
 * Auto-attach the MCP server dependencies a skill declares to the
 * project that's syncing. Per the corporate-appliance trust model,
 * publishing a skill that says "this project depends on my-grafana"
 * is enough — the client takes mcpd at its word and asks mcpd to
 * attach the server to the project.
 *
 * What this function does NOT do (deliberately):
 *   - Auto-create the server from a template if it's missing.
 *     Provisioning infrastructure from a skill push is a separate
 *     decision that needs explicit operator consent. v1 just warns
 *     when the named server doesn't exist and skips that dep.
 *   - Detach servers that a skill removed from its mcpServers list.
 *     Detach is destructive (the project loses access) and the
 *     `attach` itself is idempotent on the server side, so we err
 *     on the side of leaving things attached. PR-7 can revisit if
 *     a use case shows up.
 *
 * The mcpServers field is per-project: a skill's declared deps only
 * get attached to the project the sync is running for. Global skills
 * (no projectName context) skip this step entirely — there's no
 * project to attach to.
 */
import type { ApiClient } from '../api-client.js';
import { ApiError } from '../api-client.js';

export interface McpServerDep {
  name: string;
  fromTemplate?: string;
  project?: string;
}

export interface AttachResult {
  attached: string[];
  alreadyAttached: string[];
  missing: string[];
  errors: Array<{ server: string; error: string }>;
}

/**
 * Resolve project name → id, list its currently-attached servers,
 * then attach each declared dep that isn't already there. Idempotent
 * by virtue of the existing-attachment check.
 *
 * Failures per-server are collected, not thrown — sync continues.
 */
export async function attachSkillMcpServers(
  client: ApiClient,
  projectName: string,
  deps: McpServerDep[],
  warn: (msg: string) => void = () => {},
): Promise<AttachResult> {
  const result: AttachResult = {
    attached: [],
    alreadyAttached: [],
    missing: [],
    errors: [],
  };
  if (deps.length === 0) return result;

  // Resolve project → id (the attach endpoint is keyed by id, not name).
  let projectId: string;
  try {
    const projects = await client.get<Array<{ id: string; name: string }>>('/api/v1/projects');
    const match = projects.find((p) => p.name === projectName);
    if (!match) {
      // No project to attach to — surface every dep as an error so the
      // operator can see something is mis-configured.
      for (const dep of deps) {
        result.errors.push({ server: dep.name, error: `Project '${projectName}' not found` });
      }
      return result;
    }
    projectId = match.id;
  } catch (err: unknown) {
    for (const dep of deps) {
      result.errors.push({
        server: dep.name,
        error: `Failed to resolve project: ${err instanceof Error ? err.message : String(err)}`,
      });
    }
    return result;
  }

  // Inspect current attachments. The /api/v1/projects/:id/servers POST
  // endpoint is idempotent server-side, but we still pre-check so we
  // can report alreadyAttached vs newly-attached cleanly.
  let attached = new Set<string>();
  try {
    const project = await client.get<{ servers?: Array<{ server?: { name: string } }> }>(`/api/v1/projects/${projectId}`);
    attached = new Set(
      (project.servers ?? [])
        .map((s) => s.server?.name)
        .filter((n): n is string => typeof n === 'string'),
    );
  } catch (err: unknown) {
    warn(`mcpctl: failed to read current attachments for project '${projectName}': ${err instanceof Error ? err.message : String(err)}`);
    // Fall through with an empty set — we'll attempt attaches and let
    // server-side idempotency cover any duplicates.
  }

  // Optionally narrow the existing-server set so we can warn loudly on
  // unknown server names. (Server attaches against a non-existent
  // server would 404 anyway, but a clearer warning is friendlier.)
  let existingServers = new Set<string>();
  try {
    const servers = await client.get<Array<{ name: string }>>('/api/v1/servers');
    existingServers = new Set(servers.map((s) => s.name));
  } catch {
    // Best-effort; if listing fails we still try the attach.
  }

  for (const dep of deps) {
    // Honour an explicit `project` on the dep — defensive, normally
    // matches the active project anyway. Skip mismatches so a skill
    // can declare deps for a different project without collateral
    // damage during this sync.
    if (dep.project && dep.project !== projectName) {
      continue;
    }

    if (attached.has(dep.name)) {
      result.alreadyAttached.push(dep.name);
      continue;
    }

    if (existingServers.size > 0 && !existingServers.has(dep.name)) {
      // Server doesn't exist on mcpd. v1 doesn't auto-create; warn and continue.
      const detail = dep.fromTemplate
        ? ` (skill suggests creating it via template '${dep.fromTemplate}')`
        : '';
      warn(`mcpctl: skill mcpServers dep '${dep.name}' not found on mcpd${detail}; skipping attach`);
      result.missing.push(dep.name);
      continue;
    }

    try {
      await client.post(`/api/v1/projects/${projectId}/servers`, { server: dep.name });
      result.attached.push(dep.name);
    } catch (err: unknown) {
      // Idempotency: 409 (already attached) is success.
      if (err instanceof ApiError && err.status === 409) {
        result.alreadyAttached.push(dep.name);
        continue;
      }
      // 404 means either the project or the server vanished mid-sync.
      if (err instanceof ApiError && err.status === 404) {
        result.missing.push(dep.name);
        continue;
      }
      result.errors.push({
        server: dep.name,
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  return result;
}

/** Type-narrow the metadata.mcpServers field. Tolerant of garbage. */
export function parseMcpServerDeps(value: unknown): McpServerDep[] {
  if (!Array.isArray(value)) return [];
  const out: McpServerDep[] = [];
  for (const v of value) {
    if (v === null || typeof v !== 'object') continue;
    const obj = v as Record<string, unknown>;
    const name = obj['name'];
    if (typeof name !== 'string' || name.length === 0) continue;
    const dep: McpServerDep = { name };
    if (typeof obj['fromTemplate'] === 'string') dep.fromTemplate = obj['fromTemplate'];
    if (typeof obj['project'] === 'string') dep.project = obj['project'];
    out.push(dep);
  }
  return out;
}
