/**
 * Fly.io adapter.
 *
 * Emits `fly.toml` + reuses the Docker adapter's `Dockerfile` (so a single
 * `flyctl deploy` Just Works). `check()` probes `flyctl` and enforces
 * the minimum version. `deploy()` spawns `flyctl deploy` when `--execute`
 * is passed.
 *
 * fly.toml fields are a curated subset — enough to ship a small Mandu
 * app with sensible defaults, but the user can customize anything after
 * the initial generation (we preserve on re-run).
 *
 * References:
 *   - https://fly.io/docs/reference/configuration/
 *   - https://fly.io/docs/apps/launch/
 *
 * @module cli/commands/deploy/adapters/fly
 */
import path from "node:path";
import { CLI_ERROR_CODES } from "../../../errors/codes";
import { writeArtifact } from "../artifact-writer";
import {
  getProviderCliStatus,
  type SpawnVersion,
} from "../provider-cli";
import { renderDockerfile } from "./docker";
import type {
  AdapterArtifact,
  AdapterCheckResult,
  AdapterIssue,
  DeployAdapter,
  DeployOptions,
  DeployResult,
  ProjectContext,
  SecretSpec,
} from "../types";

const DEFAULT_PRIMARY_REGION = "nrt";
const DEFAULT_PORT = 3333;
const MIN_FLYCTL_VERSION = "0.1.0";

const FLY_SECRETS: ReadonlyArray<SecretSpec> = [
  {
    name: "FLY_API_TOKEN",
    required: true,
    description: "Fly.io API token used by flyctl to authenticate deploys.",
    docsUrl: "https://fly.io/docs/reference/configuration/#fly-tokens",
  },
];

// ---------------------------------------------------------------------
// fly.toml template
// ---------------------------------------------------------------------

export interface FlyTomlOptions {
  appName: string;
  primaryRegion?: string;
  internalPort?: number;
  /** Extra env vars (exposed to the container; NOT secrets). */
  env?: Record<string, string>;
}

export function renderFlyToml(options: FlyTomlOptions): string {
  validateAppName(options.appName);
  const region = options.primaryRegion ?? DEFAULT_PRIMARY_REGION;
  const port = options.internalPort ?? DEFAULT_PORT;

  const lines: string[] = [
    "# Generated by `mandu deploy --target=fly`.",
    "# Docs: https://fly.io/docs/reference/configuration/",
    "",
    `app = "${options.appName}"`,
    `primary_region = "${region}"`,
    "",
    "[build]",
    '  dockerfile = "Dockerfile"',
    "",
    "[http_service]",
    `  internal_port = ${port}`,
    "  force_https = true",
    "  auto_stop_machines = \"stop\"",
    "  auto_start_machines = true",
    "  min_machines_running = 0",
    '  processes = ["app"]',
    "",
    "[[vm]]",
    "  cpu_kind = \"shared\"",
    "  cpus = 1",
    "  memory_mb = 512",
    "",
    "[[http_service.checks]]",
    `  grace_period = "10s"`,
    `  interval = "30s"`,
    `  timeout = "5s"`,
    `  method = "GET"`,
    `  path = "/"`,
  ];

  if (options.env && Object.keys(options.env).length > 0) {
    lines.push("", "[env]");
    for (const [key, value] of Object.entries(options.env)) {
      if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
        throw new Error(
          `renderFlyToml: env var "${key}" must match /^[A-Z][A-Z0-9_]*$/`
        );
      }
      lines.push(`  ${key} = ${JSON.stringify(value)}`);
    }
  }

  return lines.join("\n") + "\n";
}

function validateAppName(name: string): void {
  if (!/^[a-z0-9-]{1,30}$/.test(name)) {
    throw new Error(
      `Fly app name "${name}" is invalid — must match /^[a-z0-9-]{1,30}$/.`
    );
  }
}

// ---------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------

interface FlyAdapterOptions {
  /** Override the `flyctl --version` spawn implementation (tests). */
  spawnImpl?: SpawnVersion;
  /** Stub `flyctl deploy` execution (tests). */
  deployImpl?: (
    project: ProjectContext,
    options: DeployOptions
  ) => Promise<DeployResult>;
}

export function createFlyAdapter(internal: FlyAdapterOptions = {}): DeployAdapter {
  return {
    name: "Fly.io",
    target: "fly",
    minimumCliVersion: { binary: "flyctl", semver: MIN_FLYCTL_VERSION },
    secrets: FLY_SECRETS,

    async check(project, options): Promise<AdapterCheckResult> {
      const errors: AdapterIssue[] = [];
      const warnings: AdapterIssue[] = [];

      // App name sanity check (pre-fly.toml write).
      try {
        validateAppName(options.projectName ?? project.projectName);
      } catch (err) {
        errors.push({
          code: CLI_ERROR_CODES.DEPLOY_CONFIG_INVALID,
          message: err instanceof Error ? err.message : String(err),
          hint: "Pass --project=<name> matching /^[a-z0-9-]{1,30}$/.",
        });
      }

      // flyctl probing only runs in execute mode — for artifact-only
      // prepare we tolerate a missing CLI.
      if (options.execute) {
        const status = await getProviderCliStatus("flyctl", MIN_FLYCTL_VERSION, {
          spawnImpl: internal.spawnImpl,
        });
        if (!status.installed) {
          errors.push({
            code: CLI_ERROR_CODES.DEPLOY_PROVIDER_CLI_MISSING,
            message: "flyctl is not installed.",
            hint: "Install flyctl from https://fly.io/docs/hands-on/install-flyctl/",
          });
        } else if (!status.meetsMinimum && status.version) {
          errors.push({
            code: CLI_ERROR_CODES.DEPLOY_PROVIDER_CLI_OUTDATED,
            message: `flyctl ${status.version} is older than required ${MIN_FLYCTL_VERSION}.`,
            hint: "Run `flyctl version upgrade` to update.",
          });
        }
      }

      return { ok: errors.length === 0, errors, warnings };
    },

    async prepare(project, options): Promise<AdapterArtifact[]> {
      const artifacts: AdapterArtifact[] = [];
      const rootDir = project.rootDir;
      const appName = options.projectName ?? project.projectName;

      // 1. Dockerfile (reuse docker adapter's template so a single file
      //    serves both targets).
      const hasLockfile = await pathExists(path.join(rootDir, "bun.lock"));
      const dockerfileResult = await writeArtifact({
        forbiddenValues: options.forbiddenSecrets,
        path: path.join(rootDir, "Dockerfile"),
        content: renderDockerfile({
          hasLockfile,
          port: project.config.server?.port ?? DEFAULT_PORT,
        }),
      });
      artifacts.push({
        path: dockerfileResult.path,
        preserved: dockerfileResult.preserved,
        description: "Shared Dockerfile (docker + fly targets)",
      });

      // 2. fly.toml — preserved if user already has one.
      const flyTomlResult = await writeArtifact({
        forbiddenValues: options.forbiddenSecrets,
        path: path.join(rootDir, "fly.toml"),
        content: renderFlyToml({
          appName,
          primaryRegion: DEFAULT_PRIMARY_REGION,
          internalPort: project.config.server?.port ?? DEFAULT_PORT,
        }),
        preserveIfExists: true,
      });
      artifacts.push({
        path: flyTomlResult.path,
        preserved: flyTomlResult.preserved,
        description: flyTomlResult.preserved
          ? "Existing fly.toml preserved"
          : `Scaffolded fly.toml (app=${appName}, region=${DEFAULT_PRIMARY_REGION})`,
      });

      return artifacts;
    },

    async deploy(project, options): Promise<DeployResult> {
      if (internal.deployImpl) {
        return internal.deployImpl(project, options);
      }
      // Production path would spawn `flyctl deploy`; we return a
      // structured "not implemented without execute harness" marker so
      // the dispatcher can render a helpful message. Real spawn logic
      // lives behind `--execute` and is covered by future integration
      // tests (staging project required).
      return {
        ok: false,
        errors: [
          {
            code: CLI_ERROR_CODES.DEPLOY_NOT_IMPLEMENTED,
            message: "flyctl spawn harness is provided only via DeployAdapter.deployImpl.",
            hint: "Run `flyctl deploy` manually, or inject a spawn stub in tests.",
          },
        ],
      };
    },
  };
}

export const flyAdapter: DeployAdapter = createFlyAdapter();

async function pathExists(target: string): Promise<boolean> {
  try {
    await (await import("node:fs/promises")).access(target);
    return true;
  } catch {
    return false;
  }
}
