import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import { loginCommand } from './auth.js';
import { configSetCommand } from './config.js';
import { readRegistryCredential } from '../utils/auth-store.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { MATRIX_NPM_REGISTRY_SCOPES } from '../utils/npm-registry-client.js';

export interface IRegistryCommandOptions {
  readonly cwd?: string;
  readonly registry?: string;
  readonly username?: string;
  readonly token?: string;
  readonly email?: string;
  readonly expiresAt?: string;
  readonly credentialsPath?: string;
  readonly configPath?: string;
  readonly timeoutMs?: number;
}

export interface IRegistryUseResult {
  readonly ok: true;
  readonly profile: string;
  readonly registry: string;
  readonly configPath: string;
}

export interface IRegistryLoginResult {
  readonly ok: true;
  readonly registry: string;
  readonly username: string;
  readonly credentialsPath: string;
}

export interface IRegistryDoctorResult {
  readonly ok: boolean;
  readonly mode: 'registry-doctor';
  readonly registry: {
    readonly url: string;
    readonly source: string;
    readonly reachable: boolean;
    readonly status?: number;
    readonly error?: string;
  };
  readonly credentials: {
    readonly found: boolean;
    readonly path: string;
    readonly username?: string;
    readonly authChecked: boolean;
    readonly authOk?: boolean;
    readonly whoami?: string;
    readonly error?: string;
  };
  readonly npm: {
    readonly userConfig: string;
    readonly scopeResolution: Record<string, string>;
    readonly allScopesMatch: boolean;
    readonly poisonedUserConfigIgnored: boolean;
  };
}

const LOCAL_GITEA_REGISTRY = 'http://127.0.0.1:3000/api/packages/open-matrix/npm/';

export async function registryUseCommand(
  profile: string,
  options: IRegistryCommandOptions = {},
): Promise<IRegistryUseResult> {
  const cwd = options.cwd ?? process.cwd();
  const registry = registryForProfile(profile, options.registry, cwd, options.configPath);
  const result = await configSetCommand('registry', registry, {
    cwd,
    configPath: options.configPath,
  });
  return {
    ok: true,
    profile,
    registry: result.value,
    configPath: result.configPath,
  };
}

export async function registryLoginCommand(options: IRegistryCommandOptions = {}): Promise<IRegistryLoginResult> {
  if (!options.username || !options.token) {
    throw new Error('MX_REGISTRY_LOGIN_REQUIRED: --username and --token are required');
  }
  const result = await loginCommand({
    username: options.username,
    token: options.token,
    email: options.email,
    expiresAt: options.expiresAt,
    registry: options.registry,
    credentialsPath: options.credentialsPath,
    cwd: options.cwd,
  });
  return {
    ok: true,
    registry: result.registry,
    username: result.username,
    credentialsPath: result.credentialsPath,
  };
}

export async function registryDoctorCommand(options: IRegistryCommandOptions = {}): Promise<IRegistryDoctorResult> {
  const cwd = options.cwd ?? process.cwd();
  const resolved = resolvePackageRegistry(options.registry, {
    cwd,
    configPath: options.configPath,
  });
  const credential = readRegistryCredential(cwd, {
    registry: resolved.registry,
    credentialsPath: options.credentialsPath,
  });
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-registry-doctor-'));
  try {
    const userConfig = writeDoctorNpmrc(tempDir, resolved.registry, credential.credential?.token ?? null);
    const npmRegistry = await runNpm(['config', 'get', 'registry', '--userconfig', userConfig], {
      cwd,
      registryUrl: resolved.registry,
      userConfig,
      timeoutMs: options.timeoutMs,
    });
    const scopeResolution: Record<string, string> = {};
    for (const scope of MATRIX_NPM_REGISTRY_SCOPES) {
      const result = await runNpm(['config', 'get', `${scope}:registry`, '--userconfig', userConfig], {
        cwd,
        registryUrl: resolved.registry,
        userConfig,
        timeoutMs: options.timeoutMs,
      });
      scopeResolution[scope] = result.stdout.trim();
    }
    const registryValue = npmRegistry.stdout.trim();
    const allScopesMatch = registryValue === resolved.registry
      && MATRIX_NPM_REGISTRY_SCOPES.every((scope) => scopeResolution[scope] === resolved.registry);
    const reachability = await checkRegistryReachability(resolved.registry, options.timeoutMs ?? 3_000);
    const auth = credential.credential
      ? await checkNpmWhoami(cwd, resolved.registry, userConfig, options.timeoutMs)
      : { checked: false as const };
    const ok = allScopesMatch
      && (auth.checked ? auth.ok === true : true);
    return {
      ok,
      mode: 'registry-doctor',
      registry: {
        url: resolved.registry,
        source: resolved.source,
        reachable: reachability.reachable,
        ...(reachability.status !== undefined ? { status: reachability.status } : {}),
        ...(reachability.error ? { error: reachability.error } : {}),
      },
      credentials: {
        found: Boolean(credential.credential),
        path: credential.credentialsPath,
        ...(credential.credential?.username ? { username: credential.credential.username } : {}),
        authChecked: auth.checked,
        ...(auth.checked ? { authOk: auth.ok } : {}),
        ...(auth.checked && auth.whoami ? { whoami: auth.whoami } : {}),
        ...(auth.checked && auth.error ? { error: auth.error } : {}),
      },
      npm: {
        userConfig,
        scopeResolution,
        allScopesMatch,
        poisonedUserConfigIgnored: allScopesMatch,
      },
    };
  } finally {
    fs.rmSync(tempDir, { recursive: true, force: true });
  }
}

function registryForProfile(
  profile: string,
  explicitRegistry: string | undefined,
  cwd: string,
  configPath: string | undefined,
): string {
  if (profile === 'local-gitea') {
    return resolvePackageRegistry(explicitRegistry ?? LOCAL_GITEA_REGISTRY, { cwd, configPath }).registry;
  }
  if (explicitRegistry) {
    return resolvePackageRegistry(explicitRegistry, { cwd, configPath }).registry;
  }
  if (profile.startsWith('http://') || profile.startsWith('https://')) {
    return resolvePackageRegistry(profile, { cwd, configPath }).registry;
  }
  throw new Error(`MX_REGISTRY_PROFILE_UNKNOWN: ${profile}`);
}

function writeDoctorNpmrc(tempDir: string, registry: string, token: string | null): string {
  const filePath = path.join(tempDir, '.npmrc');
  const lines = [
    `registry=${registry}`,
    ...MATRIX_NPM_REGISTRY_SCOPES.map((scope) => `${scope}:registry=${registry}`),
  ];
  if (token) {
    lines.push('always-auth=true');
    lines.push(`${registryAuthConfigKey(registry)}=${token}`);
  }
  fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
  return filePath;
}

function registryAuthConfigKey(registry: string): string {
  const parsed = new URL(registry);
  const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
  return `//${parsed.host}${pathname}:_authToken`;
}

async function checkRegistryReachability(
  registry: string,
  timeoutMs: number,
): Promise<{ readonly reachable: boolean; readonly status?: number; readonly error?: string }> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(registry, {
      method: 'GET',
      signal: controller.signal,
      headers: { accept: 'application/json,text/plain,*/*' },
    });
    return { reachable: true, status: response.status };
  } catch (error) {
    return {
      reachable: false,
      error: error instanceof Error ? error.message : String(error),
    };
  } finally {
    clearTimeout(timeout);
  }
}

async function checkNpmWhoami(
  cwd: string,
  registry: string,
  userConfig: string,
  timeoutMs: number | undefined,
): Promise<{
  readonly checked: true;
  readonly ok: boolean;
  readonly whoami?: string;
  readonly error?: string;
}> {
  const result = await runNpm(['whoami', '--registry', registry, '--userconfig', userConfig], {
    cwd,
    registryUrl: registry,
    userConfig,
    timeoutMs,
  });
  const output = [result.stderr, result.stdout]
    .map((value) => value.trim())
    .filter(Boolean)
    .join('\n');
  return {
    checked: true,
    ok: result.exitCode === 0,
    ...(result.exitCode === 0 && result.stdout.trim() ? { whoami: result.stdout.trim() } : {}),
    ...(result.exitCode === 0 ? {} : { error: output || `npm whoami exited ${result.exitCode}` }),
  };
}

async function runNpm(
  args: readonly string[],
  options: {
    readonly cwd: string;
    readonly registryUrl: string;
    readonly userConfig: string;
    readonly timeoutMs?: number;
  },
): Promise<{ readonly exitCode: number | null; readonly stdout: string; readonly stderr: string }> {
  const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
  return await new Promise((resolve, reject) => {
    const child = spawn(npmCommand, args, {
      cwd: options.cwd,
      env: {
        ...process.env,
        npm_config_userconfig: options.userConfig,
        NPM_CONFIG_USERCONFIG: options.userConfig,
        npm_config_registry: options.registryUrl,
        NPM_CONFIG_REGISTRY: options.registryUrl,
        npm_config_cache: path.join(path.dirname(options.userConfig), '.npm-cache'),
      },
      shell: false,
      stdio: ['ignore', 'pipe', 'pipe'],
      windowsHide: true,
    });
    const stdout: string[] = [];
    const stderr: string[] = [];
    const timeout = setTimeout(() => {
      child.kill('SIGTERM');
    }, options.timeoutMs ?? 10_000);
    child.stdout.setEncoding('utf8');
    child.stderr.setEncoding('utf8');
    child.stdout.on('data', (chunk: string) => stdout.push(chunk));
    child.stderr.on('data', (chunk: string) => stderr.push(chunk));
    child.on('error', (error) => {
      clearTimeout(timeout);
      reject(error);
    });
    child.on('close', (exitCode) => {
      clearTimeout(timeout);
      resolve({
        exitCode,
        stdout: stdout.join(''),
        stderr: stderr.join(''),
      });
    });
  });
}
