import { Command } from 'commander';
import { formatJson, formatYaml } from '../formatters/output.js';
import { resolveResource, resolveNameOrId } from './shared.js';
import type { ApiClient } from '../api-client.js';

export interface DescribeCommandDeps {
  client: ApiClient;
  fetchResource: (resource: string, id: string) => Promise<unknown>;
  fetchInspect?: (id: string) => Promise<unknown>;
  log: (...args: string[]) => void;
  mcplocalUrl?: string;
}

function pad(label: string, width = 18): string {
  return label.padEnd(width);
}

function formatServerDetail(server: Record<string, unknown>): string {
  const lines: string[] = [];
  lines.push(`=== Server: ${server.name} ===`);
  lines.push(`${pad('Name:')}${server.name}`);
  lines.push(`${pad('Transport:')}${server.transport ?? '-'}`);
  lines.push(`${pad('Replicas:')}${server.replicas ?? 1}`);
  if (server.dockerImage) lines.push(`${pad('Docker Image:')}${server.dockerImage}`);
  if (server.packageName) lines.push(`${pad('Package:')}${server.packageName}`);
  if (server.externalUrl) lines.push(`${pad('External URL:')}${server.externalUrl}`);
  if (server.repositoryUrl) lines.push(`${pad('Repository:')}${server.repositoryUrl}`);
  if (server.containerPort) lines.push(`${pad('Container Port:')}${server.containerPort}`);
  if (server.description) lines.push(`${pad('Description:')}${server.description}`);

  const command = server.command as string[] | null;
  if (command && command.length > 0) {
    lines.push('');
    lines.push('Command:');
    lines.push(`  ${command.join(' ')}`);
  }

  const env = server.env as Array<{ name: string; value?: string; valueFrom?: { secretRef: { name: string; key: string } } }> | undefined;
  if (env && env.length > 0) {
    lines.push('');
    lines.push('Environment:');
    const nameW = Math.max(6, ...env.map((e) => e.name.length)) + 2;
    lines.push(`  ${'NAME'.padEnd(nameW)}SOURCE`);
    for (const e of env) {
      if (e.value !== undefined) {
        lines.push(`  ${e.name.padEnd(nameW)}${e.value}`);
      } else if (e.valueFrom?.secretRef) {
        const ref = e.valueFrom.secretRef;
        lines.push(`  ${e.name.padEnd(nameW)}secret:${ref.name}/${ref.key}`);
      }
    }
  }

  const hc = server.healthCheck as { tool: string; arguments?: Record<string, unknown>; intervalSeconds?: number; timeoutSeconds?: number; failureThreshold?: number } | null;
  if (hc) {
    lines.push('');
    lines.push('Health Check:');
    lines.push(`  ${pad('Tool:', 22)}${hc.tool}`);
    if (hc.arguments && Object.keys(hc.arguments).length > 0) {
      lines.push(`  ${pad('Arguments:', 22)}${JSON.stringify(hc.arguments)}`);
    }
    lines.push(`  ${pad('Interval:', 22)}${hc.intervalSeconds ?? 60}s`);
    lines.push(`  ${pad('Timeout:', 22)}${hc.timeoutSeconds ?? 10}s`);
    lines.push(`  ${pad('Failure Threshold:', 22)}${hc.failureThreshold ?? 3}`);
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${server.id}`);
  if (server.createdAt) lines.push(`  ${pad('Created:', 12)}${server.createdAt}`);
  if (server.updatedAt) lines.push(`  ${pad('Updated:', 12)}${server.updatedAt}`);

  return lines.join('\n');
}

function formatInstanceDetail(instance: Record<string, unknown>, inspect?: Record<string, unknown>): string {
  const lines: string[] = [];
  const server = instance.server as { name: string } | undefined;
  lines.push(`=== Instance: ${server?.name ?? instance.id} ===`);
  lines.push(`${pad('Status:')}${instance.status}`);
  lines.push(`${pad('Server:')}${server?.name ?? String(instance.serverId)}`);
  lines.push(`${pad('Container ID:')}${instance.containerId ?? '-'}`);
  lines.push(`${pad('Port:')}${instance.port ?? '-'}`);

  // Health section
  const healthStatus = instance.healthStatus as string | null;
  const lastHealthCheck = instance.lastHealthCheck as string | null;
  if (healthStatus || lastHealthCheck) {
    lines.push('');
    lines.push('Health:');
    lines.push(`  ${pad('Status:', 16)}${healthStatus ?? 'unknown'}`);
    if (lastHealthCheck) lines.push(`  ${pad('Last Check:', 16)}${lastHealthCheck}`);
  }

  const metadata = instance.metadata as Record<string, unknown> | undefined;
  if (metadata && Object.keys(metadata).length > 0) {
    lines.push('');
    lines.push('Metadata:');
    for (const [key, value] of Object.entries(metadata)) {
      lines.push(`  ${pad(key + ':', 16)}${String(value)}`);
    }
  }

  if (inspect) {
    lines.push('');
    lines.push('Container:');
    for (const [key, value] of Object.entries(inspect)) {
      if (typeof value === 'object' && value !== null) {
        lines.push(`  ${key}: ${JSON.stringify(value)}`);
      } else {
        lines.push(`  ${pad(key + ':', 16)}${String(value)}`);
      }
    }
  }

  // Events section (k8s-style)
  const events = instance.events as Array<{ timestamp: string; type: string; message: string }> | undefined;
  if (events && events.length > 0) {
    lines.push('');
    lines.push('Events:');
    const tsW = 26;
    const typeW = 10;
    lines.push(`  ${'TIMESTAMP'.padEnd(tsW)}${'TYPE'.padEnd(typeW)}MESSAGE`);
    for (const ev of events) {
      lines.push(`  ${(ev.timestamp ?? '').padEnd(tsW)}${(ev.type ?? '').padEnd(typeW)}${ev.message ?? ''}`);
    }
  }

  lines.push('');
  lines.push(`  ${pad('ID:', 12)}${instance.id}`);
  if (instance.createdAt) lines.push(`  ${pad('Created:', 12)}${instance.createdAt}`);
  if (instance.updatedAt) lines.push(`  ${pad('Updated:', 12)}${instance.updatedAt}`);

  return lines.join('\n');
}

function formatProjectDetail(
  project: Record<string, unknown>,
  prompts: Array<{ name: string; priority: number; linkTarget: string | null }> = [],
  knownLlmNames?: Set<string>,
): string {
  const lines: string[] = [];
  lines.push(`=== Project: ${project.name} ===`);
  lines.push(`${pad('Name:')}${project.name}`);
  if (project.description) lines.push(`${pad('Description:')}${project.description}`);

  // Plugin config
  const proxyModel = (project.proxyModel as string | undefined) || 'default';
  const llmProvider = project.llmProvider as string | undefined;
  const llmModel = project.llmModel as string | undefined;

  lines.push('');
  lines.push('Plugin Config:');
  lines.push(`  ${pad('Plugin:', 18)}${proxyModel}`);
  if (llmProvider) {
    // As of Phase 4, llmProvider names a centralized Llm resource (see
    // `mcpctl get llms`). A value like "none" disables LLM for the project;
    // anything else that doesn't match a registered Llm falls back to the
    // registry default on consumers — flag it so operators notice.
    const resolvable = knownLlmNames === undefined
      || llmProvider === 'none'
      || knownLlmNames.has(llmProvider);
    if (resolvable) {
      lines.push(`  ${pad('LLM:', 18)}${llmProvider}`);
    } else {
      lines.push(`  ${pad('LLM:', 18)}${llmProvider}  [warning: no Llm registered with this name — will fall back to registry default]`);
    }
  }
  if (llmModel) lines.push(`  ${pad('LLM Model:', 18)}${llmModel} (override)`);

  // Servers section
  const servers = project.servers as Array<{ server: { name: string } }> | undefined;
  if (servers && servers.length > 0) {
    lines.push('');
    lines.push('Servers:');
    lines.push('  NAME');
    for (const s of servers) {
      lines.push(`  ${s.server.name}`);
    }
  }

  // Prompts section
  if (prompts.length > 0) {
    lines.push('');
    lines.push('Prompts:');
    const nameW = Math.max(4, ...prompts.map((p) => p.name.length)) + 2;
    lines.push(`  ${'NAME'.padEnd(nameW)}${'PRI'.padEnd(6)}TYPE`);
    for (const p of prompts) {
      const type = p.linkTarget ? 'link' : 'local';
      lines.push(`  ${p.name.padEnd(nameW)}${String(p.priority).padEnd(6)}${type}`);
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${project.id}`);
  if (project.ownerId) lines.push(`  ${pad('Owner:', 12)}${project.ownerId}`);
  if (project.createdAt) lines.push(`  ${pad('Created:', 12)}${project.createdAt}`);
  if (project.updatedAt) lines.push(`  ${pad('Updated:', 12)}${project.updatedAt}`);

  return lines.join('\n');
}

function formatSecretDetail(secret: Record<string, unknown>, showValues: boolean): string {
  const lines: string[] = [];
  lines.push(`=== Secret: ${secret.name} ===`);
  lines.push(`${pad('Name:')}${secret.name}`);

  const data = secret.data as Record<string, string> | undefined;
  const keyNames = Array.isArray(secret.keyNames) ? secret.keyNames as string[] : [];
  // The data row carries the actual values for plaintext-backed secrets and
  // is empty for backend-stored ones — fall back to keyNames so we can still
  // show what KEYS exist without their values.
  const knownKeys = (data && Object.keys(data).length > 0)
    ? Object.keys(data)
    : keyNames;

  if (knownKeys.length > 0) {
    lines.push('');
    lines.push('Data:');
    const keyW = Math.max(4, ...knownKeys.map((k) => k.length)) + 2;
    for (const key of knownKeys) {
      const value = data?.[key];
      const display = showValues
        ? (value ?? '(reveal failed — backend unreachable?)')
        : '***';
      lines.push(`  ${key.padEnd(keyW)}${display}`);
    }
    if (!showValues) {
      lines.push('');
      lines.push('  (use --show-values to reveal)');
    }
  } else {
    lines.push(`${pad('Data:')}(empty)`);
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${secret.id}`);
  if (secret.createdAt) lines.push(`  ${pad('Created:', 12)}${secret.createdAt}`);
  if (secret.updatedAt) lines.push(`  ${pad('Updated:', 12)}${secret.updatedAt}`);

  return lines.join('\n');
}

interface PoolMembersInfo {
  poolName: string;
  explicitPoolName: string | null;
  size: number;
  activeCount: number;
  members: Array<{ id?: string; name: string; status?: string; kind?: string; url?: string }>;
}

function formatLlmDetail(llm: Record<string, unknown>, pool?: PoolMembersInfo): string {
  const lines: string[] = [];
  lines.push(`=== LLM: ${llm.name} ===`);
  lines.push(`${pad('Name:')}${llm.name}`);
  lines.push(`${pad('Type:')}${llm.type}`);
  lines.push(`${pad('Model:')}${llm.model}`);
  lines.push(`${pad('Tier:')}${llm.tier ?? 'fast'}`);
  if (llm.url) lines.push(`${pad('URL:')}${llm.url}`);
  if (llm.description) lines.push(`${pad('Description:')}${llm.description}`);

  // v4 Pool block: only render when there's actually pool context to show.
  // For solo Llms (poolName null AND pool size 1), suppress the section so
  // describe stays compact for the common case. For explicit-pool members
  // OR rows whose name is implicitly seeding a pool (size > 1), render up
  // top so it's the first thing the operator sees — pool routing is a
  // significant behavioral fact.
  const poolNameVal = llm.poolName as string | null | undefined;
  const isExplicitPool = poolNameVal !== null && poolNameVal !== undefined && poolNameVal !== '';
  const isImplicitPool = pool !== undefined && pool.size > 1;
  if (isExplicitPool || isImplicitPool) {
    lines.push('');
    lines.push('Pool:');
    const effective = pool?.poolName ?? (poolNameVal ?? llm.name as string);
    lines.push(`  ${pad('Pool name:', 14)}${effective}${isExplicitPool ? '' : '  (implicit, falls back to name)'}`);
    if (pool !== undefined) {
      lines.push(`  ${pad('Members:', 14)}${String(pool.size)} (${String(pool.activeCount)} active)`);
      for (const m of pool.members) {
        const youSuffix = m.name === llm.name ? '  ← this row' : '';
        lines.push(`    - ${m.name}  [${m.kind ?? '?'}/${m.status ?? '?'}]${youSuffix}`);
      }
    }
  }

  const ref = llm.apiKeyRef as { name: string; key: string } | null | undefined;
  lines.push('');
  lines.push('API Key:');
  if (ref) {
    lines.push(`  ${pad('Secret:', 12)}${ref.name}`);
    lines.push(`  ${pad('Key:', 12)}${ref.key}`);
  } else {
    lines.push('  (none)');
  }

  const extra = llm.extraConfig as Record<string, unknown> | undefined;
  if (extra && Object.keys(extra).length > 0) {
    lines.push('');
    lines.push('Extra Config:');
    const keyW = Math.max(6, ...Object.keys(extra).map((k) => k.length)) + 2;
    for (const [k, v] of Object.entries(extra)) {
      let display: string;
      if (v === null || v === undefined) display = '-';
      else if (typeof v === 'object') display = JSON.stringify(v);
      else display = String(v);
      lines.push(`  ${k.padEnd(keyW)}${display}`);
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${llm.id}`);
  if (llm.createdAt) lines.push(`  ${pad('Created:', 12)}${llm.createdAt}`);
  if (llm.updatedAt) lines.push(`  ${pad('Updated:', 12)}${llm.updatedAt}`);

  return lines.join('\n');
}

function formatSecretBackendDetail(backend: Record<string, unknown>): string {
  const lines: string[] = [];
  lines.push(`=== SecretBackend: ${backend.name} ===`);
  lines.push(`${pad('Name:')}${backend.name}`);
  lines.push(`${pad('Type:')}${backend.type}`);
  lines.push(`${pad('Default:')}${backend.isDefault ? 'yes' : 'no'}`);
  if (backend.description) lines.push(`${pad('Description:')}${backend.description}`);

  const config = backend.config as Record<string, unknown> | undefined;
  if (config && Object.keys(config).length > 0) {
    lines.push('');
    lines.push('Config:');
    const keyW = Math.max(6, ...Object.keys(config).map((k) => k.length)) + 2;
    for (const [key, value] of Object.entries(config)) {
      let display: string;
      if (value === null || value === undefined) display = '-';
      else if (typeof value === 'object') display = JSON.stringify(value);
      else display = String(value);
      lines.push(`  ${key.padEnd(keyW)}${display}`);
    }
  }

  const tokenMeta = (backend.tokenMeta ?? {}) as Record<string, unknown>;
  if (tokenMeta.rotatable === true) {
    lines.push('');
    lines.push(...formatTokenHealth(tokenMeta));
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${backend.id}`);
  if (backend.createdAt) lines.push(`  ${pad('Created:', 12)}${backend.createdAt}`);
  if (backend.updatedAt) lines.push(`  ${pad('Updated:', 12)}${backend.updatedAt}`);

  return lines.join('\n');
}

/**
 * Render the Token health section for a wizard-provisioned openbao backend.
 * Returns an array of lines (caller pushes them). Stale = no successful
 * rotation in >26h (2h grace over the nominal 24h cadence).
 */
function formatTokenHealth(meta: Record<string, unknown>): string[] {
  const lines: string[] = [];
  const generatedAt = parseIso(meta.generatedAt);
  const nextRenewalAt = parseIso(meta.nextRenewalAt);
  const validUntil = parseIso(meta.validUntil);
  const lastRotationAt = parseIso(meta.lastRotationAt);
  const lastError = meta.lastRotationError as string | null | undefined;
  const now = Date.now();

  const STALE_GRACE_MS = 26 * 3600 * 1000;
  const staleByAge = lastRotationAt !== null && (now - lastRotationAt.getTime()) > STALE_GRACE_MS;
  const hasError = typeof lastError === 'string' && lastError !== '';

  let status: string;
  if (hasError && staleByAge) status = 'ERROR (stale)';
  else if (staleByAge) status = 'STALE — no successful rotation in the last cycle';
  else if (hasError) status = 'WARNING — last rotation hit an error but token is still fresh';
  else status = 'healthy';

  lines.push(`Token health:   ${status}`);
  if (generatedAt !== null) {
    lines.push(`  ${pad('Generated:', 16)}${generatedAt.toISOString()}${describeAge(generatedAt, now)}`);
  }
  if (nextRenewalAt !== null) {
    lines.push(`  ${pad('Next renewal:', 16)}${nextRenewalAt.toISOString()}${describeAge(nextRenewalAt, now)}`);
  }
  if (validUntil !== null) {
    lines.push(`  ${pad('Valid until:', 16)}${validUntil.toISOString()}${describeAge(validUntil, now)}`);
  }
  if (lastRotationAt !== null) {
    lines.push(`  ${pad('Last rotation:', 16)}${lastRotationAt.toISOString()}${describeAge(lastRotationAt, now)}`);
  }
  if (hasError) {
    lines.push(`  ${pad('Last error:', 16)}${lastError}`);
  }
  return lines;
}

function parseIso(v: unknown): Date | null {
  if (typeof v !== 'string' || v === '') return null;
  const d = new Date(v);
  return Number.isNaN(d.getTime()) ? null : d;
}

function describeAge(target: Date, now: number): string {
  const diffMs = target.getTime() - now;
  const abs = Math.abs(diffMs);
  const hours = Math.round(abs / 3600_000);
  const days = Math.round(abs / 86_400_000);
  if (abs < 60_000) return '  (just now)';
  if (abs < 3600_000) return `  (${String(Math.round(abs / 60_000))} min ${diffMs < 0 ? 'ago' : 'away'})`;
  if (hours < 48) return `  (${String(hours)}h ${diffMs < 0 ? 'ago' : 'away'})`;
  return `  (${String(days)}d ${diffMs < 0 ? 'ago' : 'away'})`;
}

function formatTemplateDetail(template: Record<string, unknown>): string {
  const lines: string[] = [];
  lines.push(`=== Template: ${template.name} ===`);
  lines.push(`${pad('Name:')}${template.name}`);
  lines.push(`${pad('Version:')}${template.version ?? '1.0.0'}`);
  lines.push(`${pad('Transport:')}${template.transport ?? 'STDIO'}`);
  lines.push(`${pad('Replicas:')}${template.replicas ?? 1}`);
  if (template.dockerImage) lines.push(`${pad('Docker Image:')}${template.dockerImage}`);
  if (template.packageName) lines.push(`${pad('Package:')}${template.packageName}`);
  if (template.externalUrl) lines.push(`${pad('External URL:')}${template.externalUrl}`);
  if (template.repositoryUrl) lines.push(`${pad('Repository:')}${template.repositoryUrl}`);
  if (template.containerPort) lines.push(`${pad('Container Port:')}${template.containerPort}`);
  if (template.description) lines.push(`${pad('Description:')}${template.description}`);

  const command = template.command as string[] | null;
  if (command && command.length > 0) {
    lines.push('');
    lines.push('Command:');
    lines.push(`  ${command.join(' ')}`);
  }

  const env = template.env as Array<{ name: string; description?: string; required?: boolean; defaultValue?: string }> | undefined;
  if (env && env.length > 0) {
    lines.push('');
    lines.push('Environment Variables:');
    const nameW = Math.max(6, ...env.map((e) => e.name.length)) + 2;
    lines.push(`  ${'NAME'.padEnd(nameW)}${'REQUIRED'.padEnd(10)}DESCRIPTION`);
    for (const e of env) {
      const req = e.required ? 'yes' : 'no';
      const desc = e.description ?? '';
      lines.push(`  ${e.name.padEnd(nameW)}${req.padEnd(10)}${desc}`);
    }
  }

  const hc = template.healthCheck as { tool: string; arguments?: Record<string, unknown>; intervalSeconds?: number; timeoutSeconds?: number; failureThreshold?: number } | null;
  if (hc) {
    lines.push('');
    lines.push('Health Check:');
    lines.push(`  ${pad('Tool:', 22)}${hc.tool}`);
    if (hc.arguments && Object.keys(hc.arguments).length > 0) {
      lines.push(`  ${pad('Arguments:', 22)}${JSON.stringify(hc.arguments)}`);
    }
    lines.push(`  ${pad('Interval:', 22)}${hc.intervalSeconds ?? 60}s`);
    lines.push(`  ${pad('Timeout:', 22)}${hc.timeoutSeconds ?? 10}s`);
    lines.push(`  ${pad('Failure Threshold:', 22)}${hc.failureThreshold ?? 3}`);
  }

  lines.push('');
  lines.push('Usage:');
  lines.push(`  mcpctl create server my-${template.name} --from-template=${template.name}`);

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${template.id}`);
  if (template.createdAt) lines.push(`  ${pad('Created:', 12)}${template.createdAt}`);
  if (template.updatedAt) lines.push(`  ${pad('Updated:', 12)}${template.updatedAt}`);

  return lines.join('\n');
}

interface RbacBinding { role: string; resource?: string; action?: string; name?: string }
interface RbacDef { name: string; subjects: Array<{ kind: string; name: string }>; roleBindings: RbacBinding[] }
interface PermissionSet { source: string; bindings: RbacBinding[] }

function formatPermissionSections(sections: PermissionSet[]): string[] {
  const lines: string[] = [];
  for (const section of sections) {
    const bindings = section.bindings;
    if (bindings.length === 0) continue;

    const resourceBindings = bindings.filter((b) => 'resource' in b && b.resource !== undefined);
    const operationBindings = bindings.filter((b) => 'action' in b && b.action !== undefined);

    if (resourceBindings.length > 0) {
      lines.push('');
      lines.push(`${section.source} — Resources:`);
      const roleW = Math.max(6, ...resourceBindings.map((b) => b.role.length)) + 2;
      const resW = Math.max(10, ...resourceBindings.map((b) => (b.resource ?? '').length)) + 2;
      const hasName = resourceBindings.some((b) => b.name);
      if (hasName) {
        lines.push(`  ${'ROLE'.padEnd(roleW)}${'RESOURCE'.padEnd(resW)}NAME`);
      } else {
        lines.push(`  ${'ROLE'.padEnd(roleW)}RESOURCE`);
      }
      for (const b of resourceBindings) {
        if (hasName) {
          lines.push(`  ${b.role.padEnd(roleW)}${(b.resource ?? '').padEnd(resW)}${b.name ?? '*'}`);
        } else {
          lines.push(`  ${b.role.padEnd(roleW)}${b.resource}`);
        }
      }
    }

    if (operationBindings.length > 0) {
      lines.push('');
      lines.push(`${section.source} — Operations:`);
      lines.push(`  ${'ACTION'.padEnd(20)}ROLE`);
      for (const b of operationBindings) {
        lines.push(`  ${(b.action ?? '').padEnd(20)}${b.role}`);
      }
    }
  }
  return lines;
}

function collectBindingsForSubject(
  rbacDefs: RbacDef[],
  kind: string,
  name: string,
): { rbacName: string; bindings: RbacBinding[] }[] {
  const results: { rbacName: string; bindings: RbacBinding[] }[] = [];
  for (const def of rbacDefs) {
    const matched = def.subjects.some((s) => s.kind === kind && s.name === name);
    if (matched) {
      results.push({ rbacName: def.name, bindings: def.roleBindings });
    }
  }
  return results;
}

function formatUserDetail(
  user: Record<string, unknown>,
  rbacDefs?: RbacDef[],
  userGroups?: string[],
): string {
  const lines: string[] = [];
  lines.push(`=== User: ${user.email} ===`);
  lines.push(`${pad('Email:')}${user.email}`);
  lines.push(`${pad('Name:')}${(user.name as string | null) ?? '-'}`);
  lines.push(`${pad('Provider:')}${(user.provider as string | null) ?? 'local'}`);

  if (userGroups && userGroups.length > 0) {
    lines.push(`${pad('Groups:')}${userGroups.join(', ')}`);
  }

  if (rbacDefs) {
    const email = user.email as string;

    // Direct permissions (User:email subjects)
    const directMatches = collectBindingsForSubject(rbacDefs, 'User', email);
    const directBindings = directMatches.flatMap((m) => m.bindings);
    const directSources = directMatches.map((m) => m.rbacName).join(', ');

    // Inherited permissions (Group:name subjects)
    const inheritedSections: PermissionSet[] = [];
    if (userGroups) {
      for (const groupName of userGroups) {
        const groupMatches = collectBindingsForSubject(rbacDefs, 'Group', groupName);
        const groupBindings = groupMatches.flatMap((m) => m.bindings);
        if (groupBindings.length > 0) {
          inheritedSections.push({ source: `Inherited (${groupName})`, bindings: groupBindings });
        }
      }
    }

    const sections: PermissionSet[] = [];
    if (directBindings.length > 0) {
      sections.push({ source: `Direct (${directSources})`, bindings: directBindings });
    }
    sections.push(...inheritedSections);

    if (sections.length > 0) {
      lines.push('');
      lines.push('Access:');
      lines.push(...formatPermissionSections(sections));
    } else {
      lines.push('');
      lines.push('Access: (none)');
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${user.id}`);
  if (user.createdAt) lines.push(`  ${pad('Created:', 12)}${user.createdAt}`);
  if (user.updatedAt) lines.push(`  ${pad('Updated:', 12)}${user.updatedAt}`);

  return lines.join('\n');
}

function formatGroupDetail(group: Record<string, unknown>, rbacDefs?: RbacDef[]): string {
  const lines: string[] = [];
  lines.push(`=== Group: ${group.name} ===`);
  lines.push(`${pad('Name:')}${group.name}`);
  if (group.description) lines.push(`${pad('Description:')}${group.description}`);

  const members = group.members as Array<{ user: { email: string }; createdAt?: string }> | undefined;
  if (members && members.length > 0) {
    lines.push('');
    lines.push('Members:');
    const emailW = Math.max(6, ...members.map((m) => m.user.email.length)) + 2;
    lines.push(`  ${'EMAIL'.padEnd(emailW)}ADDED`);
    for (const m of members) {
      const added = (m.createdAt as string | undefined) ?? '-';
      lines.push(`  ${m.user.email.padEnd(emailW)}${added}`);
    }
  }

  if (rbacDefs) {
    const groupName = group.name as string;
    const matches = collectBindingsForSubject(rbacDefs, 'Group', groupName);
    const allBindings = matches.flatMap((m) => m.bindings);
    const sources = matches.map((m) => m.rbacName).join(', ');

    if (allBindings.length > 0) {
      const sections: PermissionSet[] = [{ source: `Granted (${sources})`, bindings: allBindings }];
      lines.push('');
      lines.push('Access:');
      lines.push(...formatPermissionSections(sections));
    } else {
      lines.push('');
      lines.push('Access: (none)');
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${group.id}`);
  if (group.createdAt) lines.push(`  ${pad('Created:', 12)}${group.createdAt}`);
  if (group.updatedAt) lines.push(`  ${pad('Updated:', 12)}${group.updatedAt}`);

  return lines.join('\n');
}

function formatRbacDetail(rbac: Record<string, unknown>): string {
  const lines: string[] = [];
  lines.push(`=== RBAC: ${rbac.name} ===`);
  lines.push(`${pad('Name:')}${rbac.name}`);

  const subjects = rbac.subjects as Array<{ kind: string; name: string }> | undefined;
  if (subjects && subjects.length > 0) {
    lines.push('');
    lines.push('Subjects:');
    const kindW = Math.max(6, ...subjects.map((s) => s.kind.length)) + 2;
    lines.push(`  ${'KIND'.padEnd(kindW)}NAME`);
    for (const s of subjects) {
      lines.push(`  ${s.kind.padEnd(kindW)}${s.name}`);
    }
  }

  const roleBindings = rbac.roleBindings as Array<{ role: string; resource?: string; action?: string; name?: string }> | undefined;
  if (roleBindings && roleBindings.length > 0) {
    // Separate resource bindings from operation bindings
    const resourceBindings = roleBindings.filter((b) => 'resource' in b && b.resource !== undefined);
    const operationBindings = roleBindings.filter((b) => 'action' in b && b.action !== undefined);

    if (resourceBindings.length > 0) {
      lines.push('');
      lines.push('Resource Bindings:');
      const roleW = Math.max(6, ...resourceBindings.map((b) => b.role.length)) + 2;
      const resW = Math.max(10, ...resourceBindings.map((b) => (b.resource ?? '').length)) + 2;
      const hasName = resourceBindings.some((b) => b.name);
      if (hasName) {
        lines.push(`  ${'ROLE'.padEnd(roleW)}${'RESOURCE'.padEnd(resW)}NAME`);
      } else {
        lines.push(`  ${'ROLE'.padEnd(roleW)}RESOURCE`);
      }
      for (const b of resourceBindings) {
        if (hasName) {
          lines.push(`  ${b.role.padEnd(roleW)}${(b.resource ?? '').padEnd(resW)}${b.name ?? '*'}`);
        } else {
          lines.push(`  ${b.role.padEnd(roleW)}${b.resource}`);
        }
      }
    }

    if (operationBindings.length > 0) {
      lines.push('');
      lines.push('Operations:');
      lines.push(`  ${'ACTION'.padEnd(20)}ROLE`);
      for (const b of operationBindings) {
        lines.push(`  ${(b.action ?? '').padEnd(20)}${b.role}`);
      }
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${rbac.id}`);
  if (rbac.createdAt) lines.push(`  ${pad('Created:', 12)}${rbac.createdAt}`);
  if (rbac.updatedAt) lines.push(`  ${pad('Updated:', 12)}${rbac.updatedAt}`);

  return lines.join('\n');
}

function formatMcpTokenDetail(token: Record<string, unknown>, allRbac: RbacDef[]): string {
  const lines: string[] = [];
  lines.push(`=== McpToken: ${token.name} ===`);
  lines.push(`${pad('Name:')}${token.name}`);
  lines.push(`${pad('Project:')}${token.projectName ?? token.projectId ?? '-'}`);
  lines.push(`${pad('Status:')}${token.status ?? '-'}`);
  lines.push(`${pad('Prefix:')}${token.tokenPrefix ?? '-'}`);
  if (token.description) lines.push(`${pad('Description:')}${token.description}`);
  lines.push(`${pad('Owner:')}${token.ownerEmail ?? token.ownerId ?? '-'}`);
  lines.push(`${pad('Created:')}${token.createdAt ?? '-'}`);
  lines.push(`${pad('Last Used:')}${token.lastUsedAt ?? 'never'}`);
  lines.push(`${pad('Expires:')}${token.expiresAt ?? 'never'}`);
  if (token.revokedAt) lines.push(`${pad('Revoked At:')}${token.revokedAt}`);

  // Find the auto-created RbacDefinition (subject McpToken:<sha>) to surface bindings.
  // We don't know the sha from the describe response — match by convention: name 'mcptoken-<id>'.
  const rbacDef = allRbac.find((r) => r.name === `mcptoken-${token.id as string}`);
  if (rbacDef && Array.isArray(rbacDef.roleBindings) && rbacDef.roleBindings.length > 0) {
    lines.push('');
    lines.push('Bindings:');
    for (const b of rbacDef.roleBindings as Array<{ role: string; resource?: string; action?: string; name?: string }>) {
      if (b.action !== undefined) {
        lines.push(`  run ${b.action}`);
      } else if (b.resource !== undefined) {
        lines.push(`  ${b.role} ${b.resource}${b.name !== undefined ? `/${b.name}` : ''}`);
      }
    }
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${token.id}`);

  return lines.join('\n');
}

async function formatPromptDetail(prompt: Record<string, unknown>, client?: ApiClient): Promise<string> {
  const lines: string[] = [];
  lines.push(`=== Prompt: ${prompt.name} ===`);
  lines.push(`${pad('Name:')}${prompt.name}`);

  const proj = prompt.project as { name: string } | null | undefined;
  lines.push(`${pad('Project:')}${proj?.name ?? (prompt.projectId ? String(prompt.projectId) : '(global)')}`);
  lines.push(`${pad('Priority:')}${prompt.priority ?? 5}`);

  // Link info
  const link = prompt.linkTarget as string | null | undefined;
  if (link) {
    lines.push('');
    lines.push('Link:');
    lines.push(`  ${pad('Target:', 12)}${link}`);
    const status = prompt.linkStatus as string | null | undefined;
    if (status) lines.push(`  ${pad('Status:', 12)}${status}`);
  }

  // Content — resolve linked content if possible
  let content = prompt.content as string | undefined;
  if (link && client) {
    const resolved = await resolveLink(link, client);
    if (resolved) content = resolved;
  }

  lines.push('');
  lines.push('Content:');
  if (content) {
    // Indent content with 2 spaces for readability
    for (const line of content.split('\n')) {
      lines.push(`  ${line}`);
    }
  } else {
    lines.push('  (no content)');
  }

  lines.push('');
  lines.push('Metadata:');
  lines.push(`  ${pad('ID:', 12)}${prompt.id}`);
  if (prompt.version) lines.push(`  ${pad('Version:', 12)}${prompt.version}`);
  if (prompt.createdAt) lines.push(`  ${pad('Created:', 12)}${prompt.createdAt}`);
  if (prompt.updatedAt) lines.push(`  ${pad('Updated:', 12)}${prompt.updatedAt}`);

  return lines.join('\n');
}

/**
 * Resolve a prompt link target via mcpd proxy's resources/read.
 * Returns resolved content string or null on failure.
 */
async function resolveLink(linkTarget: string, client: ApiClient): Promise<string | null> {
  try {
    // Parse link: project/server:uri
    const slashIdx = linkTarget.indexOf('/');
    if (slashIdx < 1) return null;
    const project = linkTarget.slice(0, slashIdx);
    const rest = linkTarget.slice(slashIdx + 1);
    const colonIdx = rest.indexOf(':');
    if (colonIdx < 1) return null;
    const serverName = rest.slice(0, colonIdx);
    const uri = rest.slice(colonIdx + 1);

    // Resolve server name → ID
    const servers = await client.get<Array<{ id: string; name: string }>>(
      `/api/v1/projects/${encodeURIComponent(project)}/servers`,
    );
    const target = servers.find((s) => s.name === serverName);
    if (!target) return null;

    // Call resources/read via proxy
    const proxyResponse = await client.post<{
      result?: { contents?: Array<{ text?: string }> };
      error?: { code: number; message: string };
    }>('/api/v1/mcp/proxy', {
      serverId: target.id,
      method: 'resources/read',
      params: { uri },
    });

    if (proxyResponse.error) return null;
    const contents = proxyResponse.result?.contents;
    if (!contents || contents.length === 0) return null;
    return contents.map((c) => c.text ?? '').join('\n');
  } catch {
    return null; // Silently fall back to stored content
  }
}

function formatProxymodelDetail(model: Record<string, unknown>): string {
  const lines: string[] = [];
  const modelType = (model.type as string | undefined) ?? 'pipeline';
  lines.push(`=== ProxyModel: ${model.name} ===`);
  lines.push(`${pad('Name:')}${model.name}`);
  lines.push(`${pad('Source:')}${model.source ?? 'unknown'}`);
  lines.push(`${pad('Type:')}${modelType}`);

  if (modelType === 'plugin') {
    if (model.description) lines.push(`${pad('Description:')}${model.description}`);
    const extendsArr = model.extends as readonly string[] | undefined;
    if (extendsArr && extendsArr.length > 0) {
      lines.push(`${pad('Extends:')}${[...extendsArr].join(', ')}`);
    }
    const hooks = model.hooks as string[] | undefined;
    if (hooks && hooks.length > 0) {
      lines.push('');
      lines.push('Hooks:');
      for (const h of hooks) {
        lines.push(`  - ${h}`);
      }
    }
    return lines.join('\n');
  }

  // Pipeline type
  lines.push(`${pad('Controller:')}${model.controller ?? '-'}`);
  lines.push(`${pad('Cacheable:')}${model.cacheable ? 'yes' : 'no'}`);

  const appliesTo = model.appliesTo as string[] | undefined;
  if (appliesTo && appliesTo.length > 0) {
    lines.push(`${pad('Applies To:')}${appliesTo.join(', ')}`);
  }

  const controllerConfig = model.controllerConfig as Record<string, unknown> | undefined;
  if (controllerConfig && Object.keys(controllerConfig).length > 0) {
    lines.push('');
    lines.push('Controller Config:');
    for (const [key, value] of Object.entries(controllerConfig)) {
      lines.push(`  ${pad(key + ':', 20)}${String(value)}`);
    }
  }

  const stages = model.stages as Array<{ type: string; config?: Record<string, unknown> }> | undefined;
  if (stages && stages.length > 0) {
    lines.push('');
    lines.push('Stages:');
    for (let i = 0; i < stages.length; i++) {
      const s = stages[i]!;
      lines.push(`  ${i + 1}. ${s.type}`);
      if (s.config && Object.keys(s.config).length > 0) {
        for (const [key, value] of Object.entries(s.config)) {
          lines.push(`     ${pad(key + ':', 20)}${String(value)}`);
        }
      }
    }
  }

  return lines.join('\n');
}

function formatGenericDetail(obj: Record<string, unknown>): string {
  const lines: string[] = [];
  for (const [key, value] of Object.entries(obj)) {
    if (value === null || value === undefined) {
      lines.push(`${pad(key + ':')}  -`);
    } else if (Array.isArray(value)) {
      if (value.length === 0) {
        lines.push(`${pad(key + ':')}  []`);
      } else {
        lines.push(`${key}:`);
        for (const item of value) {
          lines.push(`  - ${typeof item === 'object' ? JSON.stringify(item) : String(item)}`);
        }
      }
    } else if (typeof value === 'object') {
      lines.push(`${key}:`);
      for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
        lines.push(`  ${pad(k + ':')}${String(v)}`);
      }
    } else {
      lines.push(`${pad(key + ':')}${String(value)}`);
    }
  }
  return lines.join('\n');
}

export function createDescribeCommand(deps: DescribeCommandDeps): Command {
  return new Command('describe')
    .description('Show detailed information about a resource')
    .argument('<resource>', 'resource type (server, project, instance)')
    .argument('<id>', 'resource ID or name')
    .option('-o, --output <format>', 'output format (detail, json, yaml)', 'detail')
    .option('--show-values', 'Show secret values (default: masked)')
    .action(async (resourceArg: string, idOrName: string, opts: { output: string; showValues?: boolean }) => {
      const resource = resolveResource(resourceArg);

      // ProxyModels are served by mcplocal, not mcpd
      if (resource === 'proxymodels') {
        const mcplocalUrl = deps.mcplocalUrl ?? 'http://localhost:3200';
        const item = await fetchProxymodelFromMcplocal(mcplocalUrl, idOrName);
        if (opts.output === 'json') {
          deps.log(formatJson(item));
        } else if (opts.output === 'yaml') {
          deps.log(formatYaml(item));
        } else {
          deps.log(formatProxymodelDetail(item));
        }
        return;
      }

      // Resolve name → ID
      let id: string;
      if (resource === 'instances') {
        // Instances: accept instance ID or server name (resolve to first running instance)
        try {
          id = await resolveNameOrId(deps.client, resource, idOrName);
        } catch {
          // Not an instance ID — try as server name
          const servers = await deps.client.get<Array<{ id: string; name: string }>>('/api/v1/servers');
          const server = servers.find((s) => s.name === idOrName || s.id === idOrName);
          if (server) {
            const instances = await deps.client.get<Array<{ id: string; status: string }>>(`/api/v1/instances?serverId=${server.id}`);
            const running = instances.find((i) => i.status === 'RUNNING') ?? instances[0];
            if (running) {
              id = running.id;
            } else {
              throw new Error(`No instances found for server '${idOrName}'`);
            }
          } else {
            id = idOrName;
          }
        }
      } else {
        // Prompts/promptrequests: let fetchResource handle scoping (it respects --project)
        if (resource === 'prompts' || resource === 'promptrequests') {
          id = idOrName;
        } else {
          try {
            id = await resolveNameOrId(deps.client, resource, idOrName);
          } catch {
            id = idOrName;
          }
        }
      }

      let item: Record<string, unknown>;
      if (resource === 'secrets' && opts.showValues === true) {
        // --show-values needs the resolved data (the raw row's `data` is
        // empty for non-plaintext backends — values live in the backend).
        // Use ?reveal=true so mcpd dispatches through the backing driver.
        item = await deps.client.get<Record<string, unknown>>(`/api/v1/secrets/${id}?reveal=true`);
      } else {
        item = await deps.fetchResource(resource, id) as Record<string, unknown>;
      }

      // Enrich instances with container inspect data
      let inspect: Record<string, unknown> | undefined;
      if (resource === 'instances' && deps.fetchInspect && item.containerId) {
        try {
          inspect = await deps.fetchInspect(id) as Record<string, unknown>;
          item.containerInspect = inspect;
        } catch {
          // Container may not be available
        }
      }

      if (opts.output === 'json') {
        deps.log(formatJson(item));
      } else if (opts.output === 'yaml') {
        deps.log(formatYaml(item));
      } else {
        // Visually clean sectioned output
        switch (resource) {
          case 'servers':
            deps.log(formatServerDetail(item));
            break;
          case 'instances':
            deps.log(formatInstanceDetail(item, inspect));
            break;
          case 'secrets':
            deps.log(formatSecretDetail(item, opts.showValues === true));
            break;
          case 'templates':
            deps.log(formatTemplateDetail(item));
            break;
          case 'secretbackends':
            deps.log(formatSecretBackendDetail(item));
            break;
          case 'llms': {
            // v4: also fetch pool membership so the describe Pool block
            // can show siblings + active counts. Best-effort — older
            // mcpd versions without the /members route 404 here, in
            // which case we render the row alone.
            let poolInfo: PoolMembersInfo | undefined;
            try {
              poolInfo = await deps.client.get<PoolMembersInfo>(
                `/api/v1/llms/${encodeURIComponent(item.name as string)}/members`,
              );
            } catch {
              // Old mcpd without /members, or RBAC denial — fall back silently.
            }
            deps.log(formatLlmDetail(item, poolInfo));
            break;
          }
          case 'projects': {
            const [projectPrompts, llms] = await Promise.all([
              deps.client
                .get<Array<{ name: string; priority: number; linkTarget: string | null }>>(`/api/v1/prompts?projectId=${item.id as string}`)
                .catch(() => []),
              deps.client
                .get<Array<{ name: string }>>('/api/v1/llms')
                .catch(() => [] as Array<{ name: string }>),
            ]);
            const llmNames = new Set(llms.map((l) => l.name));
            deps.log(formatProjectDetail(item, projectPrompts, llmNames));
            break;
          }
          case 'users': {
            // Fetch RBAC definitions and groups to show permissions
            const [rbacDefsForUser, allGroupsForUser] = await Promise.all([
              deps.client.get<RbacDef[]>('/api/v1/rbac').catch(() => [] as RbacDef[]),
              deps.client.get<Array<{ name: string; members?: Array<{ user: { email: string } }> }>>('/api/v1/groups').catch(() => []),
            ]);
            const userEmail = item.email as string;
            const userGroupNames = allGroupsForUser
              .filter((g) => g.members?.some((m) => m.user.email === userEmail))
              .map((g) => g.name);
            deps.log(formatUserDetail(item, rbacDefsForUser, userGroupNames));
            break;
          }
          case 'groups': {
            const rbacDefsForGroup = await deps.client.get<RbacDef[]>('/api/v1/rbac').catch(() => [] as RbacDef[]);
            deps.log(formatGroupDetail(item, rbacDefsForGroup));
            break;
          }
          case 'rbac':
            deps.log(formatRbacDetail(item));
            break;
          case 'prompts':
            deps.log(await formatPromptDetail(item, deps.client));
            break;
          case 'mcptokens': {
            // Fetch the auto-created RbacDefinition (if any) so bindings are visible in describe.
            const rbacForToken = await deps.client
              .get<RbacDef[]>('/api/v1/rbac')
              .catch(() => [] as RbacDef[]);
            deps.log(formatMcpTokenDetail(item, rbacForToken));
            break;
          }
          default:
            deps.log(formatGenericDetail(item));
        }
      }
    });
}

async function fetchProxymodelFromMcplocal(mcplocalUrl: string, name: string): Promise<Record<string, unknown>> {
  const http = await import('node:http');
  const url = `${mcplocalUrl}/proxymodels/${encodeURIComponent(name)}`;

  return new Promise<Record<string, unknown>>((resolve, reject) => {
    const req = http.get(url, { timeout: 5000 }, (res) => {
      let data = '';
      res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
      res.on('end', () => {
        try {
          if (res.statusCode === 404) {
            reject(new Error(`ProxyModel '${name}' not found`));
            return;
          }
          resolve(JSON.parse(data) as Record<string, unknown>);
        } catch {
          reject(new Error('Invalid response from mcplocal'));
        }
      });
    });
    req.on('error', () => reject(new Error(`Cannot connect to mcplocal at ${mcplocalUrl}`)));
    req.on('timeout', () => { req.destroy(); reject(new Error('mcplocal request timed out')); });
  });
}
