import { existsSync, readdirSync, readFileSync, realpathSync, mkdtempSync, writeFileSync, rmSync } from 'fs';
import { dirname, join, resolve, basename } from 'path';
import { tmpdir } from 'os';
import { fileURLToPath, pathToFileURL } from 'url';
import { getWorkspaceDir } from '../lib/init-command.js';
import { ensureUserDataLayout, getUserDataStrategiesDir } from '../lib/user-data.js';
import type { Plugin, OnResolveArgs } from 'esbuild';
import type { Strategy, StrategyEntry } from './types.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Resolve SDK path for user strategy imports (raw filesystem path, not URL).
// realpathSync ensures dedup against built-in strategies even under --preserve-symlinks.
const SDK_PATH = realpathSync(resolve(__dirname, '../sdk/index.ts'));

const strategyMap = new Map<string, StrategyEntry>();
const knowledgeDocMap = new Map<string, string>();
let loaded = false;

const INFRA_FILES = new Set(['loader', 'types', 'game-utils', 'player-targets', 'greeting', 'spawn', 'strategy-loop', 'new-events-backfill']);

function resolveUserStrategiesDir(): string | undefined {
  const workspaceDir = getWorkspaceDir();
  if (!existsSync(workspaceDir)) return undefined;
  ensureUserDataLayout(workspaceDir);
  const strategiesDir = getUserDataStrategiesDir(workspaceDir);
  if (!existsSync(strategiesDir)) return undefined;
  return strategiesDir;
}

/**
 * Create an esbuild plugin that intercepts `@myclaw163/clawclaw-cli` and
 * `@myclaw163/clawclaw-cli/*` imports (and the legacy bare `clawclaw-cli`
 * name for backward compat) and redirects them to the local SDK path
 * (marked external so Node resolves the same module instance at runtime).
 *
 * `require()` calls fail the build with a clear error, since `require` is
 * unavailable in ESM output and would otherwise crash at runtime.
 */
function createSdkPlugin(): { plugin: Plugin } {
  const sdkUrl = pathToFileURL(SDK_PATH).href;

  const plugin: Plugin = {
    name: 'clawclaw-cli-sdk',
    setup(build) {
      build.onResolve({ filter: /^(?:@myclaw163\/)?clawclaw-cli(?:\/.*)?$/ }, (args: OnResolveArgs) => {
        if (args.kind === 'require-call') {
          return {
            errors: [{
              text: `require('${args.path}') is not supported in user strategies. Use static imports: import { ... } from '${args.path}'.`,
            }],
          };
        }
        return { path: sdkUrl, external: true };
      });
    },
  };

  return { plugin };
}

// Returns Record<string, unknown> — slightly less precise than native import()'s
// Module type, but both are cast to StrategyEntry via `as` in loadFromDir.
export async function importUserFile(filePath: string): Promise<Record<string, unknown>> {
  const { plugin } = createSdkPlugin();
  const { build } = await import('esbuild');
  const result = await build({
    stdin: {
      contents: readFileSync(filePath, 'utf8'),
      resolveDir: dirname(filePath),
      sourcefile: basename(filePath),
      loader: filePath.endsWith('.ts') ? 'ts' : 'js',
    },
    bundle: true,
    format: 'esm',
    write: false,
    platform: 'node',
    packages: 'external',
    plugins: [plugin],
  });

  // Write to temporary file so Node's module cache deduplicates the SDK —
  // data URLs load as separate module instances, breaking instanceof checks.
  const tmpDir = mkdtempSync(join(tmpdir(), 'ccl-strategy-'));
  const tmpFile = join(tmpDir, basename(filePath).replace(/\.(ts|js)$/, '.mjs'));
  try {
    writeFileSync(tmpFile, result.outputFiles[0].text);
    return await import(pathToFileURL(tmpFile).href);
  } finally {
    try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
  }
}

async function loadFromDir(dir: string, skipInfra: boolean): Promise<void> {
  let files: string[];
  try {
    files = readdirSync(dir).filter(f => {
      if (f.startsWith('_')) return false;
      const match = f.match(/^(.+)\.(ts|js)$/);
      if (!match) return false;
      if (match[1].endsWith('.d')) return false;
      if (match[1].endsWith('.test')) return false;
      return !skipInfra || !INFRA_FILES.has(match[1]);
    });
  } catch {
    return;
  }
  for (const file of files) {
    try {
      const filePath = realpathSync(join(dir, file));
      const mod = skipInfra
        ? await import(pathToFileURL(filePath).href)
        : await importUserFile(filePath);
      const entry: StrategyEntry | undefined = mod.strategy as StrategyEntry | undefined;
      if (entry?.id && typeof entry.description === 'string' && typeof entry.create === 'function') {
        strategyMap.set(entry.id, entry);
        const sidecar = filePath.replace(/\.(ts|js)$/, '.knowledge.md');
        if (existsSync(sidecar)) knowledgeDocMap.set(entry.id, sidecar);
      }
    } catch (err) {
      if (!skipInfra) {
        // Strip temp-dir paths from error messages — they leak internal
        // implementation details (e.g., /tmp/ccl-strategy-XXXX/strategy.mjs)
        const raw = err instanceof Error ? err.message : String(err);
        const cleaned = raw.replace(/\S*ccl-strategy-[^/]*\//g, '');
        console.error(`[ccl] Failed to load user strategy ${file}: ${cleaned}`);
      }
    }
  }
}

async function ensureLoaded(): Promise<void> {
  if (loaded) return;
  loaded = true;
  await loadFromDir(__dirname, true);
  const userDir = resolveUserStrategiesDir();
  if (userDir) await loadFromDir(userDir, false);
}

export async function getStrategyEntry(idOrName: string): Promise<StrategyEntry | undefined> {
  await ensureLoaded();
  const byId = strategyMap.get(idOrName);
  if (byId) return byId;
  // Fall back to the Chinese display name (StrategyEntry.name). id always wins,
  // so this only resolves an alias when no id matched.
  for (const entry of strategyMap.values()) {
    if (entry.name === idOrName) return entry;
  }
  return undefined;
}

export async function listStrategyEntries(): Promise<StrategyEntry[]> {
  await ensureLoaded();
  return [...strategyMap.values()];
}

export async function getStrategyKnowledgeDoc(id: string): Promise<string | undefined> {
  await ensureLoaded();
  const path = knowledgeDocMap.get(id);
  if (!path || !existsSync(path)) return undefined;
  try {
    return readFileSync(path, 'utf8');
  } catch {
    return undefined;
  }
}

export async function resolveStrategy(id: string, args?: string[]): Promise<Strategy> {
  const entry = await getStrategyEntry(id);
  if (!entry) throw new Error(`Unknown strategy '${id}'. Use 'strategy list' to see available strategies.`);
  return entry.create(args);
}

/** Reset and re-scan — call after a new strategy file is placed, so the next list/get picks it up. */
export async function reload(): Promise<void> {
  loaded = false;
  strategyMap.clear();
  knowledgeDocMap.clear();
  await ensureLoaded();
}
