/**
 * script-orchestrator.ts
 *
 * Shared "resolve a user script by name + run it" helper. Used by both:
 *   - The LLM-facing RunUserScript tool (engine/tools/user-folder.ts).
 *   - The scheduler's script kind (scheduler.ts), so a periodic script run
 *     does not need to spin up an LLM agent loop.
 *
 * Responsibilities:
 *   - Path resolution under data/users/{userId}/{scripts,browser-macros}/ with
 *     traversal protection (delegated to resolveUserSubdir).
 *   - Frontmatter parsing for browser-macros to find session_profile_id.
 *   - Decrypting + loading Playwright storageState for the owning user.
 *   - Calling runUserScript() with the right runtime.
 *
 * Intentionally NOT responsible for:
 *   - Config gating (tools.user_scripts_enabled) — callers decide.
 *   - Tool-result formatting / recorder side effects — those stay in the
 *     LLM tool wrapper since they only make sense in an agent context.
 */

import { existsSync, readFileSync } from 'node:fs';
import { basename } from 'node:path';
import { resolveUserSubdir } from './paths.js';
import { parseScript } from './frontmatter.js';
import { runUserScript } from './script-runner.js';
import { loadSessionStateForUser } from './session-loader.js';
import type { BrowserSessionRepo } from '../db/browser-session-repo.js';

export type ScriptKind = 'script' | 'browser-macro';
export type ScriptSubdir = 'scripts' | 'browser-macros';
export type ScriptRuntime = 'plain' | 'playwright';

export interface ResolveScriptResult {
  scriptPath: string;
  subdir: ScriptSubdir;
  runtime: ScriptRuntime;
}

/**
 * Resolve a script name to its on-disk path. When kind is omitted, scripts/
 * is searched first, then browser-macros/.
 */
export function resolveScriptForKind(
  rootDir: string,
  userId: string,
  scriptName: string,
  kind: ScriptKind | undefined,
): ResolveScriptResult | { error: string } {
  const tryOne = (sd: ScriptSubdir): string | null => {
    try {
      const p = resolveUserSubdir(rootDir, userId, sd, scriptName);
      return existsSync(p) ? p : null;
    } catch {
      return null;
    }
  };

  if (kind === 'script') {
    const p = tryOne('scripts');
    if (!p) return { error: `script not found: scripts/${basename(scriptName)}` };
    return { scriptPath: p, subdir: 'scripts', runtime: 'plain' };
  }
  if (kind === 'browser-macro') {
    const p = tryOne('browser-macros');
    if (!p) return { error: `browser-macro not found: browser-macros/${basename(scriptName)}` };
    return { scriptPath: p, subdir: 'browser-macros', runtime: 'playwright' };
  }
  const sp = tryOne('scripts');
  if (sp) return { scriptPath: sp, subdir: 'scripts', runtime: 'plain' };
  const bp = tryOne('browser-macros');
  if (bp) return { scriptPath: bp, subdir: 'browser-macros', runtime: 'playwright' };
  return { error: `script not found: ${scriptName} (searched scripts/ and browser-macros/)` };
}

export interface ResolveAndRunOptions {
  rootDir: string;
  userId: string;
  /** Script name with or without `.js` extension. */
  name: string;
  params: Record<string, unknown>;
  /** If omitted, scripts/ is tried first, then browser-macros/. */
  kind?: ScriptKind;
  /** Required only when the resolved script is a browser-macro that declares session_profile_id. */
  sessRepo?: BrowserSessionRepo;
  masterKeyPath?: string;
  /** Child process timeout. Default 60_000. */
  timeoutMs?: number;
  /** Launch Chromium headless (browser-macros only). Default true. */
  headless?: boolean;
}

export interface ResolveAndRunSuccess {
  ok: true;
  result: unknown;
  logs: string[];
  durationMs: number;
  subdir: ScriptSubdir;
  runtime: ScriptRuntime;
  scriptPath: string;
}

export interface ResolveAndRunFailure {
  ok: false;
  error: string;
  /** When the failure was during script execution (not resolution), include what we know. */
  subdir?: ScriptSubdir;
  scriptPath?: string;
  durationMs?: number;
}

export type ResolveAndRunResult = ResolveAndRunSuccess | ResolveAndRunFailure;

/**
 * End-to-end: resolve a user-owned script by name, hydrate its session if it is
 * a browser-macro that needs one, and execute it. Never throws — failures are
 * returned as `{ ok: false, error }`.
 */
export async function resolveAndRunUserScript(opts: ResolveAndRunOptions): Promise<ResolveAndRunResult> {
  const { rootDir, userId, params, kind, sessRepo, masterKeyPath, timeoutMs, headless } = opts;
  const name = opts.name.endsWith('.js') ? opts.name : `${opts.name}.js`;

  const resolved = resolveScriptForKind(rootDir, userId, name, kind);
  if ('error' in resolved) {
    return { ok: false, error: resolved.error };
  }
  const { scriptPath, subdir, runtime } = resolved;

  let storageState: object | undefined;
  if (subdir === 'browser-macros') {
    let sessionProfileId: number | undefined;
    try {
      const source = readFileSync(scriptPath, 'utf-8');
      sessionProfileId = parseScript(source).frontmatter.sessionProfileId;
    } catch (err) {
      return {
        ok: false,
        error: `failed to parse script frontmatter: ${(err as Error).message}`,
        subdir,
        scriptPath,
      };
    }

    if (sessionProfileId !== undefined) {
      if (!sessRepo || !masterKeyPath) {
        return {
          ok: false,
          error:
            'session profile required but BrowserSessionRepo not configured. ' +
            'Falling back is recommended — try BrowseWeb manually.',
          subdir,
          scriptPath,
        };
      }
      const sessionResult = await loadSessionStateForUser(
        { sessRepo, masterKeyPath },
        userId,
        sessionProfileId,
      );
      if (!sessionResult.ok) {
        return { ok: false, error: sessionResult.error.message, subdir, scriptPath };
      }
      storageState = sessionResult.storageState;
    }
  }

  const start = Date.now();
  try {
    const runResult = await runUserScript({
      scriptPath,
      params,
      runtime,
      storageState,
      timeoutMs: timeoutMs ?? 60_000,
      ...(headless !== undefined ? { headless } : {}),
    });
    return {
      ok: true,
      result: runResult.result,
      logs: runResult.logs,
      durationMs: runResult.durationMs,
      subdir,
      runtime,
      scriptPath,
    };
  } catch (err) {
    return {
      ok: false,
      error: (err as Error).message,
      subdir,
      scriptPath,
      durationMs: Date.now() - start,
    };
  }
}
