#!/usr/bin/env node
import { Command } from 'commander';
import { APP_NAME, APP_VERSION } from '@mcpctl/shared';
import { createConfigCommand } from './commands/config.js';
import { createStatusCommand } from './commands/status.js';
import { createGetCommand } from './commands/get.js';
import { createDescribeCommand } from './commands/describe.js';
import { createDeleteCommand } from './commands/delete.js';
import { createLogsCommand } from './commands/logs.js';
import { createApplyCommand } from './commands/apply.js';
import { createTestCommand } from './commands/test-mcp.js';
import { createCreateCommand } from './commands/create.js';
import { createEditCommand } from './commands/edit.js';
import { createBackupCommand } from './commands/backup.js';
import { createLoginCommand, createLogoutCommand } from './commands/auth.js';
import { createAttachServerCommand, createDetachServerCommand, createApproveCommand } from './commands/project-ops.js';
import { createFavouritesCommand } from './commands/favourites.js';
import { createMcpCommand } from './commands/mcp.js';
import { createPatchCommand } from './commands/patch.js';
import { createConsoleCommand } from './commands/console/index.js';
import { createCacheCommand } from './commands/cache.js';
import { createProviderCommand } from './commands/provider.js';
import { createChatCommand } from './commands/chat.js';
import { createChatLlmCommand } from './commands/chat-llm.js';
import { createMigrateCommand } from './commands/migrate.js';
import { createRotateCommand } from './commands/rotate.js';
import { createReviewCommand } from './commands/review.js';
import { createSkillsCommand } from './commands/skills.js';
import { createPasswdCommand } from './commands/passwd.js';
import { createErrorsCommand } from './commands/errors.js';
import { ApiClient, ApiError } from './api-client.js';
import { loadConfig } from './config/index.js';
import { loadCredentials } from './auth/index.js';
import { resolveNameOrId } from './commands/shared.js';

export function createProgram(): Command {
  const program = new Command()
    .name(APP_NAME)
    .description('Manage MCP servers like kubectl manages containers')
    .version(APP_VERSION, '-v, --version')
    .enablePositionalOptions()
    .option('--daemon-url <url>', 'mcplocal daemon URL')
    .option('--direct', 'bypass mcplocal and connect directly to mcpd')
    .option('-p, --project <name>', 'Target project for project commands');

  program.addCommand(createStatusCommand());
  program.addCommand(createLoginCommand());
  program.addCommand(createLogoutCommand());

  // Resolve target URL: --direct goes to mcpd, default goes to mcplocal.
  //
  // Commander's `program.opts()` returns the default values until
  // `program.parse(argv)` runs — but commands (and ApiClient) need the
  // resolved baseUrl at construction time. The chicken-and-egg meant
  // `--direct` was previously a no-op for ApiClient: every command went to
  // mcplocal regardless. Some commands accidentally still worked because
  // mcplocal forwards plain `/api/v1/*` to mcpd, but flows that need direct
  // SSE streaming (e.g. `mcpctl chat`) went to mcplocal:3200, which doesn't
  // route them.
  //
  // Fix: peek at process.argv directly for the two global flags we need
  // before Commander's full parse runs.
  const config = loadConfig();
  const creds = loadCredentials();
  const argv = process.argv;
  const directFlag = argv.includes('--direct');
  const daemonUrlIdx = argv.indexOf('--daemon-url');
  const daemonUrlVal = daemonUrlIdx > -1 && daemonUrlIdx + 1 < argv.length
    ? argv[daemonUrlIdx + 1]
    : undefined;
  let baseUrl: string;
  if (daemonUrlVal !== undefined) {
    baseUrl = daemonUrlVal;
  } else if (directFlag) {
    baseUrl = config.mcpdUrl;
  } else {
    baseUrl = config.mcplocalUrl;
  }

  const client = new ApiClient({ baseUrl, token: creds?.token ?? undefined });

  program.addCommand(createConfigCommand(undefined, {
    client,
    credentialsDeps: {},
    log: (...args) => console.log(...args),
  }));

  const fetchResource = async (resource: string, nameOrId?: string, opts?: { project?: string; all?: boolean }): Promise<unknown[]> => {
    const projectName = opts?.project ?? program.opts().project as string | undefined;

    // Virtual resource: serverattachments (composed from project data)
    if (resource === 'serverattachments') {
      type ProjectWithServers = { name: string; id: string; servers?: Array<{ server: { name: string } }> };
      let projects: ProjectWithServers[];
      if (projectName) {
        const projectId = await resolveNameOrId(client, 'projects', projectName);
        const project = await client.get<ProjectWithServers>(`/api/v1/projects/${projectId}`);
        projects = [project];
      } else {
        projects = await client.get<ProjectWithServers[]>('/api/v1/projects');
      }
      const attachments: Array<{ project: string; server: string }> = [];
      for (const p of projects) {
        if (p.servers) {
          for (const ps of p.servers) {
            attachments.push({ server: ps.server.name, project: p.name });
          }
        }
      }
      return attachments;
    }

    // --project scoping for servers: show only attached servers
    if (!nameOrId && resource === 'servers' && projectName) {
      const projectId = await resolveNameOrId(client, 'projects', projectName);
      return client.get<unknown[]>(`/api/v1/projects/${projectId}/servers`);
    }

    // --project scoping for prompts and promptrequests
    if (!nameOrId && (resource === 'prompts' || resource === 'promptrequests')) {
      if (projectName) {
        return client.get<unknown[]>(`/api/v1/${resource}?project=${encodeURIComponent(projectName)}`);
      }
      // Default: global-only. --all (-A) shows everything.
      if (!opts?.all) {
        return client.get<unknown[]>(`/api/v1/${resource}?scope=global`);
      }
    }

    // --project scoping for mcptokens
    if (!nameOrId && resource === 'mcptokens' && projectName) {
      return client.get<unknown[]>(`/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`);
    }

    // Chat threads are project-scoped history — `get threads --project <name>`.
    if (!nameOrId && resource === 'threads') {
      if (!projectName) {
        throw new Error('threads are scoped — pass --project <name> (see `mcpctl chat --project`)');
      }
      return client.get<unknown[]>(`/api/v1/projects/${encodeURIComponent(projectName)}/threads`);
    }

    // Name-based lookup for mcptokens: names are unique only within a project
    if (nameOrId && resource === 'mcptokens' && !/^c[a-z0-9]{24}/.test(nameOrId)) {
      if (!projectName) {
        throw new Error('mcptoken names are scoped to a project — pass --project <name> or use the token id (cuid)');
      }
      const items = await client.get<Array<{ id: string; name: string }>>(
        `/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`,
      );
      const match = items.find((i) => i.name === nameOrId);
      if (!match) throw new Error(`mcptoken '${nameOrId}' not found in project '${projectName}'`);
      const item = await client.get(`/api/v1/mcptokens/${match.id}`);
      return [item];
    }

    if (nameOrId) {
      // Glob pattern — use query param filtering
      if (nameOrId.includes('*')) {
        return client.get<unknown[]>(`/api/v1/${resource}?name=${encodeURIComponent(nameOrId)}`);
      }
      let id: string;
      try {
        id = await resolveNameOrId(client, resource, nameOrId);
      } catch {
        id = nameOrId;
      }
      const item = await client.get(`/api/v1/${resource}/${id}`);
      return [item];
    }
    return client.get<unknown[]>(`/api/v1/${resource}`);
  };

  const fetchSingleResource = async (resource: string, nameOrId: string): Promise<unknown> => {
    const projectName = program.opts().project as string | undefined;

    // Prompts: resolve within project scope (or global-only without --project)
    if (resource === 'prompts' || resource === 'promptrequests') {
      const scope = projectName
        ? `?project=${encodeURIComponent(projectName)}`
        : '?scope=global';
      const items = await client.get<Array<Record<string, unknown>>>(`/api/v1/${resource}${scope}`);
      const match = items.find((item) => item.name === nameOrId);
      if (!match) {
        throw new Error(`${resource.replace(/s$/, '')} '${nameOrId}' not found${projectName ? ` in project '${projectName}'` : ' (global scope). Use --project to specify a project'}`);
      }
      return client.get(`/api/v1/${resource}/${match.id as string}`);
    }

    // Mcptokens: names are project-scoped. CUIDs pass straight through.
    if (resource === 'mcptokens' && !/^c[a-z0-9]{24}/.test(nameOrId)) {
      if (!projectName) {
        throw new Error('mcptoken names are scoped to a project — pass --project <name> or use the token id (cuid)');
      }
      const items = await client.get<Array<Record<string, unknown>>>(
        `/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`,
      );
      const match = items.find((item) => item.name === nameOrId);
      if (!match) throw new Error(`mcptoken '${nameOrId}' not found in project '${projectName}'`);
      return client.get(`/api/v1/mcptokens/${match.id as string}`);
    }

    let id: string;
    try {
      id = await resolveNameOrId(client, resource, nameOrId);
    } catch {
      id = nameOrId;
    }
    return client.get(`/api/v1/${resource}/${id}`);
  };

  program.addCommand(createGetCommand({
    fetchResource,
    log: (...args) => console.log(...args),
    getProject: () => program.opts().project as string | undefined,
    mcplocalUrl: config.mcplocalUrl,
  }));

  program.addCommand(createDescribeCommand({
    client,
    fetchResource: fetchSingleResource,
    fetchInspect: async (id: string) => client.get(`/api/v1/instances/${id}/inspect`),
    log: (...args) => console.log(...args),
    mcplocalUrl: config.mcplocalUrl,
  }));

  program.addCommand(createDeleteCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createLogsCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createCreateCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createEditCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createApplyCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createChatCommand({
    client,
    baseUrl,
    ...(creds?.token !== undefined ? { token: creds.token } : {}),
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createChatLlmCommand({
    client,
    baseUrl,
    ...(creds?.token !== undefined ? { token: creds.token } : {}),
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createPatchCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createPasswdCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createErrorsCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createBackupCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  const projectOpsDeps = {
    client,
    log: (...args: string[]) => console.log(...args),
    getProject: () => program.opts().project as string | undefined,
  };
  program.addCommand(createAttachServerCommand(projectOpsDeps), { hidden: true });
  program.addCommand(createDetachServerCommand(projectOpsDeps), { hidden: true });
  program.addCommand(createApproveCommand(projectOpsDeps));
  program.addCommand(createFavouritesCommand(projectOpsDeps));
  // PR-4: reviewer queue for proposed prompts + skills.
  program.addCommand(createReviewCommand({
    client,
    log: (...args) => console.log(...args),
  }));
  // PR-5: skills sync to ~/.claude/skills/ on demand or via SessionStart hook.
  program.addCommand(createSkillsCommand({
    client,
    log: (...args) => console.log(...args),
  }));
  program.addCommand(createMcpCommand({
    getProject: () => program.opts().project as string | undefined,
  }), { hidden: true });

  program.addCommand(createConsoleCommand({
    getProject: () => program.opts().project as string | undefined,
  }));

  program.addCommand(createCacheCommand({
    log: (...args) => console.log(...args),
    mcplocalUrl: config.mcplocalUrl,
  }));

  program.addCommand(createProviderCommand({
    log: (...args) => console.log(...args),
    mcplocalUrl: config.mcplocalUrl,
  }));

  program.addCommand(createTestCommand({
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createMigrateCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  program.addCommand(createRotateCommand({
    client,
    log: (...args) => console.log(...args),
  }));

  return program;
}

// Run when invoked directly
const isDirectRun =
  typeof process !== 'undefined' &&
  process.argv[1] !== undefined &&
  import.meta.url === `file://${process.argv[1]}`;

if (isDirectRun) {
  createProgram().parseAsync(process.argv).catch((err: unknown) => {
    if (err instanceof ApiError) {
      if (err.status === 401) {
        console.error("Error: you need to log in. Run 'mcpctl login' to authenticate.");
      } else if (err.status === 403) {
        console.error('Error: permission denied. You do not have access to this resource.');
      } else {
        let msg: string;
        try {
          const parsed = JSON.parse(err.body) as { error?: string; message?: string; details?: unknown };
          msg = parsed.error ?? parsed.message ?? err.body;
          if (parsed.details && Array.isArray(parsed.details)) {
            const issues = parsed.details as Array<{ message?: string; path?: string[] }>;
            const detail = issues.map((i) => {
              const path = i.path?.join('.') ?? '';
              return path ? `${path}: ${i.message}` : (i.message ?? '');
            }).filter(Boolean).join('; ');
            if (detail) msg += `: ${detail}`;
          }
        } catch {
          msg = err.body;
        }
        console.error(`Error: ${msg}`);
      }
    } else if (err instanceof Error) {
      console.error(`Error: ${err.message}`);
    } else {
      console.error(`Error: ${String(err)}`);
    }
    process.exit(1);
  });
}
