import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
  packageNameToPath,
  resolveRegistryRoot,
  readManifestFromPackageDir,
  readVersionFromPackageDir,
  resolvePackagesNodeModulesRoot,
  validateResolvedManifest,
} from '../utils/package-store.js';
import { extractTarball } from '../utils/archive.js';
import { resolveRegistryTarball } from '../utils/registry-store.js';
import {
  MATRIX_NPM_REGISTRY_SCOPES,
  npmPackageSpecifier,
  prepareNpmRegistryTarball,
} from '../utils/npm-registry-client.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { readRegistryCredential } from '../utils/auth-store.js';

export interface IInstallCommandOptions {
  readonly cwd?: string;
  readonly packagesDir?: string;
  readonly registryDir?: string;
  readonly registry?: string;
  readonly credentialsPath?: string;
  readonly force?: boolean;
}

export interface IInstallCommandResult {
  readonly packageName: string;
  readonly source: string;
  readonly version: string;
  readonly targetDir: string;
  readonly lifecyclePhases: readonly string[];
}

type JsonRecord = Record<string, unknown>;
type LifecyclePhase = 'validate' | 'migrate' | 'seed' | 'verify';

interface DependencyInstallOptions {
  readonly install: boolean;
  readonly registry?: string;
  readonly credentialsPath?: string;
  readonly cwd: string;
}

interface LifecycleHook {
  readonly script: string;
  readonly description?: string;
}

interface InstallLifecycle {
  readonly validate?: LifecycleHook;
  readonly migrate?: LifecycleHook;
  readonly seed?: LifecycleHook;
  readonly verify?: LifecycleHook;
}

interface RuntimeNpmDependencies {
  readonly dependencies?: JsonRecord;
  readonly optionalDependencies?: JsonRecord;
}

function parsePackageSpecifier(specifier: string): { packageName: string; version?: string } {
  if (specifier.startsWith('@')) {
    const slashIndex = specifier.indexOf('/');
    if (slashIndex === -1) {
      return { packageName: specifier };
    }
    const atIndex = specifier.lastIndexOf('@');
    if (atIndex > slashIndex) {
      return {
        packageName: specifier.slice(0, atIndex),
        version: specifier.slice(atIndex + 1),
      };
    }
    return { packageName: specifier };
  }

  const atIndex = specifier.lastIndexOf('@');
  if (atIndex > 0) {
    return {
      packageName: specifier.slice(0, atIndex),
      version: specifier.slice(atIndex + 1),
    };
  }

  return { packageName: specifier };
}

function locateExtractedPackageDir(extractedRoot: string): string {
  const packageDir = path.join(extractedRoot, 'package');
  if (fs.existsSync(path.join(packageDir, 'matrix.json')) || fs.existsSync(path.join(packageDir, 'package.json'))) {
    return packageDir;
  }
  if (fs.existsSync(path.join(extractedRoot, 'matrix.json')) || fs.existsSync(path.join(extractedRoot, 'package.json'))) {
    return extractedRoot;
  }

  const entries = fs.readdirSync(extractedRoot, { withFileTypes: true });
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const candidate = path.join(extractedRoot, entry.name);
    if (fs.existsSync(path.join(candidate, 'matrix.json')) || fs.existsSync(path.join(candidate, 'package.json'))) {
      return candidate;
    }
  }

  throw new Error(`Unable to locate package manifest inside extracted tarball: ${extractedRoot}`);
}

function asRecord(value: unknown): JsonRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }
  return value as JsonRecord;
}

function readLifecycleHook(value: unknown, phase: LifecyclePhase): LifecycleHook | undefined {
  const record = asRecord(value);
  if (!record) {
    return undefined;
  }

  const script = typeof record.script === 'string' ? record.script.trim() : '';
  if (!script) {
    throw new Error(`install.${phase}.script must be a non-empty string`);
  }

  const description = typeof record.description === 'string' ? record.description.trim() : '';
  return description ? { script, description } : { script };
}

function readInstallLifecycle(manifest: JsonRecord): InstallLifecycle {
  const install = asRecord(manifest.install);
  if (!install) {
    return {};
  }

  return {
    validate: readLifecycleHook(install.validate, 'validate'),
    migrate: readLifecycleHook(install.migrate, 'migrate'),
    seed: readLifecycleHook(install.seed, 'seed'),
    verify: readLifecycleHook(install.verify, 'verify'),
  };
}

function readPackageJson(packageDir: string): JsonRecord | null {
  const packageJsonPath = path.join(packageDir, 'package.json');
  if (!fs.existsSync(packageJsonPath)) {
    return null;
  }

  const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as unknown;
  return asRecord(parsed);
}

function hasDependencyEntries(value: unknown): boolean {
  const record = asRecord(value);
  return Boolean(record && Object.keys(record).length > 0);
}

function readRuntimeNpmDependencies(packageDir: string): RuntimeNpmDependencies | null {
  const packageJson = readPackageJson(packageDir);
  if (!packageJson) {
    return null;
  }
  const dependencies = hasDependencyEntries(packageJson.dependencies)
    ? asRecord(packageJson.dependencies) ?? undefined
    : undefined;
  const optionalDependencies = hasDependencyEntries(packageJson.optionalDependencies)
    ? asRecord(packageJson.optionalDependencies) ?? undefined
    : undefined;

  if (!dependencies && !optionalDependencies) {
    return null;
  }
  return {
    ...(dependencies ? { dependencies } : {}),
    ...(optionalDependencies ? { optionalDependencies } : {}),
  };
}

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

export function dependencyInstallNpmrcLines(params: {
  readonly registry?: string;
  readonly token?: string;
}): readonly string[] {
  const lines = [
    'registry=https://registry.npmjs.org/',
  ];
  if (params.registry) {
    for (const scope of MATRIX_NPM_REGISTRY_SCOPES) {
      lines.push(`${scope}:registry=${params.registry}`);
    }
    if (params.token) {
      lines.push('always-auth=true');
      lines.push(`${registryAuthConfigKey(params.registry)}=${params.token}`);
    }
  }
  return lines;
}

function writeDependencyInstallNpmrc(params: {
  readonly tempDir: string;
  readonly registry?: string;
  readonly token?: string;
}): string {
  const npmrcPath = path.join(params.tempDir, '.npmrc');
  const lines = dependencyInstallNpmrcLines(params);
  fs.writeFileSync(npmrcPath, `${lines.join('\n')}\n`, 'utf8');
  return npmrcPath;
}

function writeEmptyNpmGlobalConfig(tempDir: string): string {
  const npmrcPath = path.join(tempDir, 'empty-global-npmrc');
  fs.writeFileSync(npmrcPath, '', 'utf8');
  return npmrcPath;
}

function runtimeDependencyPackageName(packageName: string): string {
  const normalized = packageName.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+/, '');
  return `mx-runtime-deps-${normalized || 'package'}`;
}

function installRuntimeNpmDependencies(params: {
  readonly packageName: string;
  readonly packageDir: string;
  readonly dependencyOptions: DependencyInstallOptions;
  readonly lifecyclePhases: string[];
}): void {
  const runtimeDependencies = readRuntimeNpmDependencies(params.packageDir);
  if (!params.dependencyOptions.install || !runtimeDependencies) {
    return;
  }

  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-deps-'));
  const packageJsonPath = path.join(params.packageDir, 'package.json');
  const originalPackageJson = fs.readFileSync(packageJsonPath, 'utf8');
  try {
    fs.writeFileSync(
      packageJsonPath,
      `${JSON.stringify({
        private: true,
        name: runtimeDependencyPackageName(params.packageName),
        version: '0.0.0',
        ...runtimeDependencies,
      }, null, 2)}\n`,
      'utf8',
    );

    const credential = params.dependencyOptions.registry
      ? readRegistryCredential(params.dependencyOptions.cwd, {
        registry: params.dependencyOptions.registry,
        credentialsPath: params.dependencyOptions.credentialsPath,
      }).credential
      : null;
    const userConfig = writeDependencyInstallNpmrc({
      tempDir,
      registry: params.dependencyOptions.registry,
      token: credential?.token,
    });
    const globalConfig = writeEmptyNpmGlobalConfig(tempDir);

    const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
    const result = spawnSync(npmCommand, [
      'install',
      '--omit=dev',
      '--omit=peer',
      '--no-audit',
      '--fund=false',
      '--package-lock=false',
      '--userconfig',
      userConfig,
      '--globalconfig',
      globalConfig,
    ], {
      cwd: params.packageDir,
      encoding: 'utf8',
      env: {
        ...process.env,
        npm_config_cache: path.join(tempDir, '.npm-cache'),
        NPM_CONFIG_USERCONFIG: userConfig,
        npm_config_userconfig: userConfig,
        NPM_CONFIG_GLOBALCONFIG: globalConfig,
        npm_config_globalconfig: globalConfig,
      },
      windowsHide: true,
    });

    if (result.status !== 0) {
      const stderr = result.stderr?.trim();
      const stdout = result.stdout?.trim();
      const output = [stderr, stdout].filter(Boolean).join('\n');
      throw new Error(
        `${params.packageName} npm dependency install failed`
        + (output ? `\n${output}` : '')
      );
    }

    params.lifecyclePhases.push('dependencies');
  } finally {
    fs.writeFileSync(packageJsonPath, originalPackageJson, 'utf8');
    fs.rmSync(tempDir, { recursive: true, force: true });
  }
}

function runInstallLifecycle(params: {
  lifecycle: InstallLifecycle;
  packageName: string;
  version: string;
  packageDir: string;
  packagesRoot: string;
  installSource: string;
  lifecyclePhases: string[];
}): void {
  const phases: LifecyclePhase[] = ['validate', 'migrate', 'seed', 'verify'];

  for (const phase of phases) {
    const hook = params.lifecycle[phase];
    if (!hook) continue;

    const scriptPath = path.join(params.packageDir, hook.script);
    if (!fs.existsSync(scriptPath)) {
      throw new Error(`${params.packageName} install.${phase}.script does not exist (${hook.script})`);
    }

    const result = spawnSync(process.execPath, [scriptPath], {
      cwd: params.packageDir,
      encoding: 'utf8',
      env: {
        ...process.env,
        MATRIX_PACKAGES_DIR: params.packagesRoot,
        MATRIX_PACKAGE_DIR: params.packageDir,
        MATRIX_PACKAGE_NAME: params.packageName,
        MATRIX_PACKAGE_VERSION: params.version,
        MATRIX_INSTALL_SOURCE: params.installSource,
        MATRIX_INSTALL_PHASE: phase,
      },
      windowsHide: true,
    });

    if (result.status !== 0) {
      const stderr = result.stderr?.trim();
      const stdout = result.stdout?.trim();
      const output = [stderr, stdout].filter(Boolean).join('\n');
      throw new Error(
        `${params.packageName} install.${phase} failed`
        + (output ? `\n${output}` : '')
      );
    }

    params.lifecyclePhases.push(phase);
  }
}

function installFromDirectory(
  sourceDir: string,
  packagesRoot: string,
  nodeModulesRoot: string,
  force: boolean,
  installSource: string,
  dependencyOptions: DependencyInstallOptions,
): { packageName: string; version: string; targetDir: string; lifecyclePhases: string[] } {
  const { packageName, manifest } = readManifestFromPackageDir(sourceDir);
  validateResolvedManifest(packageName, manifest);
  const version = readVersionFromPackageDir(sourceDir);
  const lifecycle = readInstallLifecycle(manifest);

  const targetDir = path.join(nodeModulesRoot, packageNameToPath(packageName));
  const stageDir = `${targetDir}.installing-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
  const backupDir = `${targetDir}.backup-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
  const hadExistingTarget = fs.existsSync(targetDir);
  if (hadExistingTarget && !force) {
    throw new Error(`Package already installed: ${packageName} (${targetDir})`);
  }

  fs.mkdirSync(path.dirname(stageDir), { recursive: true });
  fs.cpSync(sourceDir, stageDir, { recursive: true });

  const lifecyclePhases: string[] = [];
  try {
    if (hadExistingTarget) {
      fs.renameSync(targetDir, backupDir);
    }
    fs.renameSync(stageDir, targetDir);
    installRuntimeNpmDependencies({
      packageName,
      packageDir: targetDir,
      dependencyOptions,
      lifecyclePhases,
    });
    runInstallLifecycle({
      lifecycle,
      packageName,
      version,
      packageDir: targetDir,
      packagesRoot,
      installSource,
      lifecyclePhases,
    });
    if (hadExistingTarget && fs.existsSync(backupDir)) {
      fs.rmSync(backupDir, { recursive: true, force: true });
    }
  } catch (error) {
    if (fs.existsSync(stageDir)) {
      fs.rmSync(stageDir, { recursive: true, force: true });
    }
    if (fs.existsSync(targetDir)) {
      fs.rmSync(targetDir, { recursive: true, force: true });
    }
    if (hadExistingTarget && fs.existsSync(backupDir)) {
      fs.renameSync(backupDir, targetDir);
    }
    throw error;
  }

  return {
    packageName,
    version,
    targetDir,
    lifecyclePhases,
  };
}

function runtimeEntryFromManifest(manifest: JsonRecord): string | null {
  const runtime = asRecord(manifest.runtime);
  const entry = typeof runtime?.entry === 'string' ? runtime.entry.trim() : '';
  return entry.length > 0 ? entry : null;
}

function hasWebappSection(manifest: JsonRecord): boolean {
  return asRecord(manifest.webapp) !== null;
}

function readWebappConfig(manifest: JsonRecord): { distDir: string; entryFile: string } | null {
  const webapp = asRecord(manifest.webapp);
  if (!webapp) return null;

  const distDir = typeof webapp.distDir === 'string' && webapp.distDir.trim().length > 0
    ? webapp.distDir.trim()
    : 'dist';
  const entryFile = typeof webapp.entry === 'string' && webapp.entry.trim().length > 0
    ? webapp.entry.trim()
    : 'index.html';
  return { distDir, entryFile };
}

function walkFiles(rootDir: string): string[] {
  if (!fs.existsSync(rootDir)) {
    return [];
  }

  const results: string[] = [];
  const stack = [rootDir];
  while (stack.length > 0) {
    const current = stack.pop()!;
    const entries = fs.readdirSync(current, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(current, entry.name);
      if (entry.isDirectory()) {
        stack.push(fullPath);
      } else if (entry.isFile()) {
        results.push(fullPath);
      }
    }
  }
  return results;
}

async function maybeBuildLocalTypescriptPackage(sourceDir: string): Promise<string> {
  const { manifest } = readManifestFromPackageDir(sourceDir);
  const runtimeEntry = runtimeEntryFromManifest(manifest);
  const webapp = readWebappConfig(manifest);
  if (!runtimeEntry && !webapp) {
    return sourceDir;
  }

  const sourceEntry = runtimeEntry ? path.join(sourceDir, runtimeEntry) : null;
  const webappEntryPath = webapp ? path.join(sourceDir, webapp.distDir, webapp.entryFile) : null;
  if ((sourceEntry && fs.existsSync(sourceEntry)) && (!webappEntryPath || fs.existsSync(webappEntryPath))) {
    return sourceDir;
  }

  const srcDir = path.join(sourceDir, 'src');
  if (!fs.existsSync(srcDir)) {
    return sourceDir;
  }

  const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-build-'));
  fs.cpSync(sourceDir, stagingDir, { recursive: true });

  const stagedSrcDir = path.join(stagingDir, 'src');
  const stagedRuntimeEntry = runtimeEntry ? path.join(stagingDir, runtimeEntry) : null;
  const stagedDistDir = stagedRuntimeEntry
    ? path.dirname(stagedRuntimeEntry)
    : path.join(stagingDir, webapp?.distDir ?? 'dist');
  fs.mkdirSync(stagedDistDir, { recursive: true });

  const ts = await import('typescript');
  for (const filePath of walkFiles(stagedSrcDir)) {
    const relativePath = path.relative(stagedSrcDir, filePath);
    const extension = path.extname(filePath).toLowerCase();
    const outputPath = path.join(
      stagedDistDir,
      relativePath.replace(/\.(mts|cts|tsx|ts)$/i, '.js'),
    );
    fs.mkdirSync(path.dirname(outputPath), { recursive: true });

    if (extension === '.ts' || extension === '.tsx' || extension === '.mts' || extension === '.cts') {
      const source = fs.readFileSync(filePath, 'utf8');
      const transpiled = ts.transpileModule(source, {
        compilerOptions: {
          target: ts.ScriptTarget.ES2022,
          module: ts.ModuleKind.ESNext,
          moduleResolution: ts.ModuleResolutionKind.NodeNext,
          esModuleInterop: true,
          sourceMap: false,
          inlineSourceMap: false,
          declaration: false,
        },
        fileName: filePath,
      });
      fs.writeFileSync(outputPath, transpiled.outputText, 'utf8');
      continue;
    }

    fs.copyFileSync(filePath, path.join(stagedDistDir, relativePath));
  }

  if (webapp) {
    const sourceHtmlPath = path.join(stagingDir, webapp.entryFile);
    const stagedHtmlPath = path.join(stagingDir, webapp.distDir, webapp.entryFile);
    if (fs.existsSync(sourceHtmlPath) && !fs.existsSync(stagedHtmlPath)) {
      let html = fs.readFileSync(sourceHtmlPath, 'utf8');
      html = html.replace(
        /(<script[^>]+src=["'])\.\/src\/([^"']+)\.(?:ts|tsx|mts|cts)(["'][^>]*>)/g,
        '$1./$2.js$3',
      );
      fs.mkdirSync(path.dirname(stagedHtmlPath), { recursive: true });
      fs.writeFileSync(stagedHtmlPath, html, 'utf8');
    }
  }

  return stagingDir;
}

export async function installCommand(packagePath: string, options: IInstallCommandOptions = {}): Promise<IInstallCommandResult> {
  const cwd = path.resolve(options.cwd ?? process.cwd());
  const packagesRoot = path.resolve(cwd, options.packagesDir ?? path.join(os.homedir(), '.matrix', 'packages'));
  const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
  fs.mkdirSync(nodeModulesRoot, { recursive: true });

  const normalizedPackagePath = packagePath.trim();
  if (!normalizedPackagePath) {
    throw new Error('Install source is required');
  }
  const resolvedPath = path.resolve(cwd, normalizedPackagePath);
  const force = Boolean(options.force);

  if (fs.existsSync(resolvedPath)) {
    if (fs.statSync(resolvedPath).isDirectory()) {
      const preparedSourceDir = await maybeBuildLocalTypescriptPackage(resolvedPath);
      try {
        const installed = installFromDirectory(preparedSourceDir, packagesRoot, nodeModulesRoot, force, resolvedPath, {
          install: false,
          cwd,
        });
        return {
          packageName: installed.packageName,
          source: resolvedPath,
          version: installed.version,
          targetDir: installed.targetDir,
          lifecyclePhases: installed.lifecyclePhases,
        };
      } finally {
        if (preparedSourceDir !== resolvedPath) {
          fs.rmSync(preparedSourceDir, { recursive: true, force: true });
        }
      }
    }

    if (resolvedPath.endsWith('.tar.gz') || resolvedPath.endsWith('.tgz')) {
      const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-tarball-'));
      try {
        extractTarball(resolvedPath, extractRoot);
        const sourceDir = locateExtractedPackageDir(extractRoot);
        const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, resolvedPath, {
          install: true,
          registry: resolvePackageRegistry(options.registry, { cwd }).registry,
          credentialsPath: options.credentialsPath,
          cwd,
        });
        return {
          packageName: installed.packageName,
          source: resolvedPath,
          version: installed.version,
          targetDir: installed.targetDir,
          lifecyclePhases: installed.lifecyclePhases,
        };
      } finally {
        fs.rmSync(extractRoot, { recursive: true, force: true });
      }
    }

    throw new Error(`Unsupported install source: ${resolvedPath}`);
  }

  const { packageName, version } = parsePackageSpecifier(normalizedPackagePath);

  if (options.registry && options.registryDir) {
    throw new Error('MX_REGISTRY_INVALID: use either --registry or --registry-dir, not both');
  }

  if (!options.registryDir || options.registry) {
    const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
    const specifier = npmPackageSpecifier(packageName, version);
    const fetched = await prepareNpmRegistryTarball(
      specifier,
      registry,
      cwd,
      options.credentialsPath,
    );
    const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-npm-extract-'));
    try {
      extractTarball(fetched.tarballPath, extractRoot);
      const sourceDir = locateExtractedPackageDir(extractRoot);
      const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, specifier, {
        install: true,
        registry,
        credentialsPath: options.credentialsPath,
        cwd,
      });
      return {
        packageName: installed.packageName,
        source: `${installed.packageName}@${installed.version}`,
        version: installed.version,
        targetDir: installed.targetDir,
        lifecyclePhases: installed.lifecyclePhases,
      };
    } finally {
      fs.rmSync(extractRoot, { recursive: true, force: true });
      fs.rmSync(fetched.cleanupDir, { recursive: true, force: true });
    }
  }

  const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
  const resolved = resolveRegistryTarball(registryRoot, packageName, version);
  const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-registry-'));
  try {
    extractTarball(resolved.tarballPath, extractRoot);
    const sourceDir = locateExtractedPackageDir(extractRoot);
    const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, `${packageName}@${resolved.version}`, {
      install: true,
      registry: resolvePackageRegistry(undefined, { cwd }).registry,
      cwd,
    });
    return {
      packageName: installed.packageName,
      source: `${packageName}@${resolved.version}`,
      version: installed.version,
      targetDir: installed.targetDir,
      lifecyclePhases: installed.lifecyclePhases,
    };
  } finally {
    fs.rmSync(extractRoot, { recursive: true, force: true });
  }
}
