import * as fs from 'node:fs';
import * as path from 'node:path';
import {
  packageNameToPath,
  readVersionFromPackageDir,
  resolvePackagesNodeModulesRoot,
} from '../utils/package-store.js';
import { computeDirectorySha256 } from '../utils/archive.js';

type JsonRecord = Record<string, unknown>;

interface IForkedFromRecord {
  readonly origin: string;
  readonly originVersion: string;
  readonly forkedAt: string;
  readonly forkedBy: string;
  readonly originSha256: string;
}

interface INameRename {
  readonly oldName: string;
  readonly newName: string;
}

export interface IForkCommandOptions {
  readonly cwd?: string;
  readonly packagesDir?: string;
  readonly name?: string;
  readonly replaceOrigin?: boolean;
  readonly forkedBy?: string;
}

export interface IForkCommandResult {
  readonly sourcePackageName: string;
  readonly forkedPackageName: string;
  readonly sourceDir: string;
  readonly targetDir: string;
}

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

function toPrefix(targetPackageName: string): { readonly mountPrefix: string; readonly typePrefix: string } {
  const scoped = targetPackageName.startsWith('@')
    ? targetPackageName.slice(1).split('/')[0]
    : targetPackageName.split('/')[0];
  const mountPrefix = (scoped || 'fork').replace(/[^a-zA-Z0-9]+/g, '.').replace(/\.+/g, '.').replace(/^\./, '').toLowerCase();
  const typePrefix = mountPrefix
    .split('.')
    .filter(Boolean)
    .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
    .join('');
  return {
    mountPrefix: mountPrefix || 'fork',
    typePrefix: typePrefix || 'Fork',
  };
}

function renameType(original: unknown, typePrefix: string): string | undefined {
  if (typeof original !== 'string') return undefined;
  const trimmed = original.trim();
  if (!trimmed) return undefined;
  return trimmed.startsWith(typePrefix) ? trimmed : `${typePrefix}${trimmed}`;
}

function renameMount(original: unknown, mountPrefix: string): string | undefined {
  if (typeof original !== 'string') return undefined;
  const trimmed = original.trim();
  if (!trimmed) return undefined;
  return trimmed.startsWith(`${mountPrefix}.`) ? trimmed : `${mountPrefix}.${trimmed}`;
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function deriveForkName(sourcePackageName: string): string {
  if (sourcePackageName.startsWith('@')) {
    const [, rawBase = 'package'] = sourcePackageName.split('/');
    return `@fork/${rawBase}`;
  }
  return `${sourcePackageName}-fork`;
}

function readJson(filePath: string): JsonRecord {
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
  const record = asRecord(parsed);
  if (!record) {
    throw new Error(`Expected JSON object in ${filePath}`);
  }
  return record;
}

function writeJson(filePath: string, payload: JsonRecord): void {
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
}

function rewriteMatrixManifest(
  targetDir: string,
  targetPackageName: string,
  replaceOrigin: boolean,
): ReadonlyArray<INameRename> {
  const matrixJsonPath = path.join(targetDir, 'matrix.json');
  if (!fs.existsSync(matrixJsonPath)) {
    return [];
  }

  const manifest = readJson(matrixJsonPath);
  const renames: INameRename[] = [];
  manifest.name = targetPackageName;
  if (!replaceOrigin && Array.isArray(manifest.components)) {
    const { mountPrefix, typePrefix } = toPrefix(targetPackageName);
    manifest.components = manifest.components.map((entry) => {
      const component = asRecord(entry) ?? {};
      const renamedType = renameType(component.type, typePrefix);
      const renamedExport = renameType(component.export, typePrefix);
      const renamedMount = renameMount(component.mount, mountPrefix);
      if (typeof component.type === 'string' && renamedType && renamedType !== component.type) {
        renames.push({ oldName: component.type, newName: renamedType });
      }
      if (typeof component.export === 'string' && renamedExport && renamedExport !== component.export) {
        renames.push({ oldName: component.export, newName: renamedExport });
      }
      return {
        ...component,
        type: renamedType ?? component.type,
        export: renamedExport ?? component.export,
        mount: renamedMount ?? component.mount,
      };
    });
  }

  writeJson(matrixJsonPath, manifest);
  return renames;
}

function rewritePackageJsonName(targetDir: string, targetPackageName: string): void {
  const packageJsonPath = path.join(targetDir, 'package.json');
  if (!fs.existsSync(packageJsonPath)) {
    return;
  }
  const packageJson = readJson(packageJsonPath);
  packageJson.name = targetPackageName;
  writeJson(packageJsonPath, packageJson);
}

function copyPackageTree(sourceDir: string, targetDir: string): void {
  fs.mkdirSync(path.dirname(targetDir), { recursive: true });
  fs.cpSync(sourceDir, targetDir, {
    recursive: true,
    filter: (entry) => {
      const base = path.basename(entry);
      return base !== '.git' && base !== 'node_modules' && base !== '.forked-from';
    },
  });
}

function rewriteSourceExports(targetDir: string, renames: ReadonlyArray<INameRename>): void {
  if (renames.length === 0) {
    return;
  }

  const uniqueRenames = Array.from(
    new Map(renames.map((entry) => [`${entry.oldName}=>${entry.newName}`, entry] as const)).values()
  );

  const filesToRewrite: string[] = [];
  const queue = [targetDir];
  while (queue.length > 0) {
    const current = queue.pop()!;
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      if (entry.name === 'node_modules' || entry.name === '.git') {
        continue;
      }
      const absolute = path.join(current, entry.name);
      if (entry.isDirectory()) {
        queue.push(absolute);
      } else if (entry.isFile() && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
        filesToRewrite.push(absolute);
      }
    }
  }

  for (const filePath of filesToRewrite) {
    let content = fs.readFileSync(filePath, 'utf8');
    let changed = false;
    for (const rename of uniqueRenames) {
      const pattern = new RegExp(`\\b${escapeRegExp(rename.oldName)}\\b`, 'g');
      if (pattern.test(content)) {
        content = content.replace(pattern, rename.newName);
        changed = true;
      }
    }
    if (changed) {
      fs.writeFileSync(filePath, content, 'utf8');
    }
  }
}

function pruneIfEmpty(dirPath: string): void {
  if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
    return;
  }
  if (fs.readdirSync(dirPath).length === 0) {
    fs.rmdirSync(dirPath);
  }
}

export async function forkCommand(sourcePackageName: string, options: IForkCommandOptions = {}): Promise<IForkCommandResult> {
  const cwd = path.resolve(options.cwd ?? process.cwd());
  const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
  const sourceDir = path.join(nodeModulesRoot, packageNameToPath(sourcePackageName));
  if (!fs.existsSync(sourceDir)) {
    throw new Error(`MX_FORK_SOURCE_MISSING: ${sourcePackageName}`);
  }

  const targetPackageName = options.name?.trim() || deriveForkName(sourcePackageName);
  const targetDir = path.join(nodeModulesRoot, packageNameToPath(targetPackageName));
  if (fs.existsSync(targetDir)) {
    throw new Error(`Target package already exists: ${targetPackageName}`);
  }

  copyPackageTree(sourceDir, targetDir);
  rewritePackageJsonName(targetDir, targetPackageName);
  const renames = rewriteMatrixManifest(targetDir, targetPackageName, Boolean(options.replaceOrigin));
  if (!options.replaceOrigin) {
    rewriteSourceExports(targetDir, renames);
  }

  const record: IForkedFromRecord = {
    origin: sourcePackageName,
    originVersion: readVersionFromPackageDir(sourceDir),
    forkedAt: new Date().toISOString(),
    forkedBy: options.forkedBy ?? process.env.USER ?? 'unknown',
    originSha256: computeDirectorySha256(sourceDir),
  };
  fs.writeFileSync(path.join(targetDir, '.forked-from'), JSON.stringify(record, null, 2), 'utf8');

  if (options.replaceOrigin && path.resolve(sourceDir) !== path.resolve(targetDir)) {
    fs.rmSync(sourceDir, { recursive: true, force: true });
    pruneIfEmpty(path.dirname(sourceDir));
  }

  return {
    sourcePackageName,
    forkedPackageName: targetPackageName,
    sourceDir,
    targetDir,
  };
}
