import { execSync } from "node:child_process";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import arg from "arg";
import { config } from "dotenv";
import type { AgentResult } from "./agents/shared.ts";
import { type Inputs, main } from "./main.ts";
import { defineFixture } from "./test/utils.ts";
import { log } from "./utils/cli.ts";
import { runInDocker } from "./utils/docker.ts";
import { ensureGitHubToken } from "./utils/github.ts";
import { isInsideDocker } from "./utils/globals.ts";
import { setupTestRepo } from "./utils/setup.ts";

/**
 * default play fixture for ad-hoc testing.
 * change this freely without affecting any tests.
 */
export const playFixture = defineFixture(
  {
    prompt: `What is 2 + 2? Reply with just the number.`,
    effort: "mini",
  },
  { localOnly: true }
);

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

// load action's .env file in case it exists for local dev
config();
// also load .env from repo root (for monorepo structure)
config({ path: join(__dirname, "..", ".env") });

export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult> {
  await ensureGitHubToken();

  // create unique temp directory path in OS temp location for parallel execution
  // use a parent dir from mkdtemp, then clone into a 'repo' subdirectory
  const tempParent = await mkdtemp(join(tmpdir(), "pullfrog-play-"));
  const tempDir = join(tempParent, "repo");
  const originalCwd = process.cwd();

  try {
    setupTestRepo({ tempDir });
    process.chdir(tempDir);

    // run repo setup commands if provided (for pre-planting test state like symlinks).
    // this runs AFTER clone but BEFORE the agent, simulating pre-existing repo content.
    if (process.env.PULLFROG_TEST_REPO_SETUP) {
      log.info("» running repo setup commands...");
      execSync(process.env.PULLFROG_TEST_REPO_SETUP, { cwd: tempDir, stdio: "pipe" });
    }

    // set GITHUB_WORKSPACE to tempDir so main() doesn't try to chdir to the CI checkout path
    process.env.GITHUB_WORKSPACE = tempDir;

    // allow passing full Inputs object or just a prompt string
    const inputs: Inputs =
      typeof inputsOrPrompt === "string" ? { prompt: inputsOrPrompt } : inputsOrPrompt;

    // set INPUT_* env vars for @actions/core.getInput()
    for (const [key, value] of Object.entries(inputs)) {
      if (value !== undefined && value !== null) {
        process.env[`INPUT_${key.toUpperCase()}`] = String(value);
      }
    }

    const result = await main();

    process.chdir(originalCwd);

    if (result.success) {
      log.success("Action completed successfully");
      return { success: true, output: result.output || undefined, error: undefined };
    } else {
      log.error(`Action failed: ${result.error || "Unknown error"}`);
      return { success: false, error: result.error || undefined, output: undefined };
    }
  } catch (err) {
    const errorMessage = (err as Error).message;
    log.error(`Error: ${errorMessage}`);
    return { success: false, error: errorMessage, output: undefined };
  } finally {
    // cleanup temp directory - use sudo rm because sandbox isolation may create
    // files with different ownership that rmSync can't delete
    process.chdir(originalCwd);
    try {
      execSync(`sudo rm -rf "${tempParent}"`, { stdio: "ignore" });
    } catch {
      // ignore - cleanup failure is not critical
    }
  }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const args = arg({
    "--help": Boolean,
    "--raw": String,
    "--local": Boolean,
    "-h": "--help",
    "-l": "--local",
  });

  if (args["--help"]) {
    log.info(`
Usage: node play.ts [options]

Test the Pullfrog action with the inline playFixture.

Options:
  --raw [input]           Use raw string as prompt, or JSON object as full fixture
  --local, -l             Run locally (default: runs in Docker)
  -h, --help              Show this help message

Environment:
  PLAY_LOCAL=1            Same as --local

Examples:
  node play.ts                                              # Run inline playFixture
  node play.ts --raw "Hello world"                          # Use raw string as prompt
  node play.ts --raw '{"prompt":"Hello","timeout":"5s"}'    # Use JSON fixture
    `);
    process.exit(0);
  }

  // default: run in Docker (unless --local, PLAY_LOCAL=1, or already inside Docker)
  const useLocal = args["--local"] || process.env.PLAY_LOCAL === "1" || isInsideDocker;

  if (!useLocal) {
    const passArgs = process.argv
      .slice(2)
      .map((a) => `'${a.replace(/'/g, "'\\''")}'`)
      .join(" ");
    const nodeCmd = `node play.ts ${passArgs}`;

    // use agent-specific volume to avoid conflicts when running in parallel
    const agentOverride = process.env.AGENT_OVERRIDE ?? "default";
    const volumeName = `pullfrog-action-node-modules-${agentOverride}`;

    const result = runInDocker({
      actionDir: __dirname,
      args: process.argv.slice(2),
      nodeCmd,
      volumeName,
      envFilterMode: "passthrough",
      onStart: () => log.info("» running in Docker container..."),
    });

    process.exit(result.status ?? 1);
  }

  if (args["--raw"]) {
    const raw = args["--raw"];
    // try to parse as JSON, otherwise treat as prompt string
    let input: Inputs | string = raw;
    try {
      input = JSON.parse(raw) as Inputs;
    } catch {
      // not valid JSON, use as prompt string
    }
    const result = await run(input);
    process.exit(result.success ? 0 : 1);
  }

  // no args - use inline playFixture
  const result = await run(playFixture);
  process.exit(result.success ? 0 : 1);
}
