import type { Tool, ToolStreamEvent } from '@wrongstack/core';
import { detectPackageEcosystem, recordPackageAction } from '@wrongstack/core';
import { join } from 'node:path';
import { spawnStream } from './_spawn-stream.js';
import { detectPackageManager, normalizeCommandOutput, safeResolve } from './_util.js';

interface InstallInput {
  packages?: string | string[] | undefined;
  save?: 'dependency' | 'dev' | 'optional' | undefined;
  cwd?: string | undefined;
  dry_run?: boolean | undefined;
  global?: boolean | undefined;
}

interface InstallOutput {
  packages: string[];
  exit_code: number;
  output: string;
  dry_run: boolean;
  truncated: boolean;
}

export const installTool: Tool<InstallInput, InstallOutput> = {
  name: 'install',
  category: 'Package Management',
  description:
    'Install, update or manage packages using the detected package manager (pnpm/npm/yarn). ' +
    'Strongly preferred over raw shell commands for dependency management because it is structured and safer.',
  usageHint:
    'ALWAYS USE THIS INSTEAD OF BASH FOR PACKAGE WORK:\n\n' +
    '- Empty `packages` → normal `install` (respects lockfile).\n' +
    '- Provide names → adds/updates specific packages.\n' +
    '- `dry_run: true` for safe preview.\n' +
    '- Set `save` appropriately.\n' +
    'This tool has proper capability declaration and is heavily recommended in the security posture of the project.',
  permission: 'confirm',
  mutating: true,
  riskTier: 'standard',
  timeoutMs: 120_000,
  capabilities: ['package.install', 'shell.restricted'],
  inputSchema: {
    type: 'object',
    properties: {
      packages: {
        type: 'string',
        description:
          'Package(s) to install: single name, comma-separated list, or empty for all deps',
      },
      save: {
        type: 'string',
        enum: ['dependency', 'dev', 'optional'],
        description: 'Where to save the package(s): "dependency", "devDependencies", or "optionalDependencies".',
      },
      cwd: {
        type: 'string',
        description: 'Working directory for the install command (must stay inside project).',
      },
      dry_run: {
        type: 'boolean',
        description: 'If true, show what would be installed without actually modifying package.json or node_modules.',
      },
      global: {
        type: 'boolean',
        description: 'Whether to perform a global install (use with caution).',
      },
    },
  },
  async execute(input, ctx, opts) {
    let final: InstallOutput | undefined;
    const executeStream = installTool.executeStream;
    if (!executeStream) throw new Error('installTool: stream execution unavailable');
    for await (const ev of executeStream(input, ctx, opts)) {
      if (ev.type === 'final') final = ev.output;
    }
    if (!final) throw new Error('install: stream ended without final event');
    return final;
  },
  async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<InstallOutput>> {
    const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
    const pkgManager = await detectPackageManager(cwd);
    yield { type: 'log', text: `Resolving with ${pkgManager}…`, data: { phase: 'resolve' } };

    const save = input.save === 'dev' ? '-D' : input.save === 'optional' ? '-O' : '';
    const globalFlag = input.global ? ['-g'] : [];

    const args: string[] = [];
    if (input.dry_run) args.push('--dry-run');
    if (pkgManager === 'pnpm') {
      if (save) args.push(save);
      args.push('add', ...globalFlag);
    } else if (pkgManager === 'yarn') {
      args.push('add', ...globalFlag);
    } else {
      args.push('install', ...globalFlag);
    }

    const pkgList = input.packages
      ? (Array.isArray(input.packages) ? input.packages : input.packages.split(',')).map((p) =>
          p.trim(),
        )
      : [];

    // Validate package names to prevent flag injection and path traversal.
    // A name like "--ignore-scripts=false" would be interpreted as a flag;
    // "file:../../etc/passwd" as a local path specifier.
    // Cap at 200 chars to prevent ReDoS on the regex engine (npm's max is 214).
    const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
    for (const pkg of pkgList) {
      if (!PKG_NAME_RE.test(pkg) || pkg.startsWith('-') || pkg.length > 200) {
        yield {
          type: 'final',
          output: {
            packages: pkgList,
            exit_code: 1,
            output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
            dry_run: Boolean(input.dry_run),
            truncated: false,
          },
        };
        return;
      }
    }

    if (pkgList.length > 0) args.push(...pkgList);

    yield {
      type: 'log',
      text: `Fetching ${pkgList.length || 'all'} packages…`,
      data: { phase: 'fetch' },
    };

    const result = yield* spawnStream({
      cmd: pkgManager,
      args,
      cwd,
      signal: opts.signal,
      maxBytes: 100_000,
    });

    const output: InstallOutput = {
      packages: pkgList,
      exit_code: result.exitCode,
      output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ''),
      dry_run: args.includes('--dry-run'),
      truncated: result.truncated,
    };

    // Record package authorship after a successful, non-dry-run install.
    // Skip global installs (no manifest modification) and dry runs.
    const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
    if (isSuccess && pkgList.length > 0) {
      const trackerOpts = ctx.meta?.['packageTrackerOpts'] as {
        storageDir: string;
        projectRoot: string;
      } | undefined;
      if (trackerOpts) {
        const manifestPath = resolveManifestPath(cwd, pkgManager);
        for (const pkg of pkgList) {
          try {
            await recordPackageAction(trackerOpts, {
              manifestPath,
              packageName: pkg,
              versionSpec: 'latest', // exact version resolved by package manager at install time
              ecosystem: detectPackageEcosystem(manifestPath),
              agentId: ctx.agentId,
              agentName: ctx.agentName,
              sessionId: ctx.session.id,
            });
          } catch {
            // Best-effort — a failed record doesn't fail the install
          }
        }
      }
    }

    yield { type: 'final', output };
  },
};

function resolveManifestPath(cwd: string, pkgManager: string): string {
  switch (pkgManager) {
    case 'pnpm':
    case 'yarn':
    case 'npm':
      return join(cwd, 'package.json');
    default:
      return join(cwd, 'package.json');
  }
}

