import { exec } from "node:child_process";
import { promisify } from "node:util";
import { Sandbox, Volume } from "microsandbox";
import type { SandboxHandle } from "microsandbox";
import type { LogReadSource } from "microsandbox";
import type { EnsureSandboxInput, ListSandboxesQuery, ShellExecInput } from "../schemas/sandbox";
import type { SandboxLogsInput } from "../schemas/sandbox";
import type { SandboxRuntimeMetrics } from "../types/runtime-service";
import { getMsbPath } from "../lib/msb";
import { SandboxRegistry } from "./SandboxRegistry";

const execAsync = promisify(exec);

type VolumeConfig = NonNullable<EnsureSandboxInput["volumes"]>[string];

type RuntimeNative = {
  fs(): {
    readString(path: string): Promise<string>;
    readToString(path: string): Promise<string>;
    write(path: string, data: Buffer): Promise<void>;
    list(path: string): Promise<Array<{ path: string; kind: string; size?: number; modified?: unknown }>>;
    read(path: string): Promise<Buffer | Uint8Array | string>;
    mkdir(path: string): Promise<void>;
  };
  exec(cmd: string, args?: string[]): Promise<{
    stdout(): string;
    stderr(): string;
    get code(): number;
  }>;
  shell(script: string): Promise<{
    stdout(): string;
    stderr(): string;
    get code(): number;
  }>;
  stop(): Promise<void>;
  kill(): Promise<void>;
};

type SandboxConfig = {
  name: string;
  image: unknown;
  memoryMib?: number;
  cpus?: number;
  env?: unknown;
  volumes?: Record<string, unknown>;
};

function parseConfigJson(configJson: string): SandboxConfig {
  try {
    const raw = JSON.parse(configJson);
    const rawVolumes = raw.volumes ?? raw.mounts;
    const volumes: Record<string, unknown> = {};
    if (Array.isArray(rawVolumes)) {
      for (const mount of rawVolumes) {
        if (mount && typeof mount === "object" && "guest" in mount) {
          volumes[String(mount.guest)] = mount;
        }
      }
    } else if (rawVolumes && typeof rawVolumes === "object") {
      Object.assign(volumes, rawVolumes);
    }
    return {
      name: raw.name ?? "",
      image: raw.image ?? "",
      memoryMib: raw.memoryMib ?? raw.memory_mib,
      cpus: raw.cpus,
      env: raw.env ?? {},
      volumes,
    };
  } catch {
    return { name: "", image: "" };
  }
}

function msToIsoString(ms: number | Date | null | undefined): string {
  if (ms == null) return "";
  if (ms instanceof Date) return ms.toISOString();
  return new Date(ms).toISOString();
}

type MsbInspectEnvVolumes = {
  env: unknown;
  volumes: Record<string, unknown>;
};

function parseMsbInspectEnvVolumes(stdout: string): MsbInspectEnvVolumes {
  const data = JSON.parse(stdout);
  const env = data?.config?.env ?? {};

  const volumes: Record<string, unknown> = {};
  const rawMounts = data?.config?.mounts;
  if (Array.isArray(rawMounts)) {
    for (const mount of rawMounts) {
      if (mount && typeof mount === "object" && "guest" in mount) {
        volumes[String(mount.guest)] = mount;
      }
    }
  }

  return { env, volumes };
}

export class MicrosandboxRuntimeService {
  constructor(private deps: { registry?: SandboxRegistry } = {}) {}

  private get registry(): SandboxRegistry {
    return this.deps.registry ?? (this.deps.registry = new SandboxRegistry());
  }

  private resolvePath(path: string): string {
    if (path === "~" || path === "~/") {
      return "/";
    }
    if (path.startsWith("~/")) {
      return `/${path.slice(2)}`;
    }
    return path;
  }

  private toListItem(info: {
    name: string;
    status: string;
    configJson: string;
    createdAt?: number | Date | null;
    updatedAt?: number | Date | null;
  }): {
    name: string;
    status: string;
    image: unknown;
    cpus?: number;
    memoryMib?: number;
    envCount: number;
    volumeCount: number;
    createdAt: string;
    updatedAt: string;
  } {
    const config = parseConfigJson(info.configJson);
    const env = config.env ? (Array.isArray(config.env) ? config.env : config.env) : [];
    const envCount = Array.isArray(env) ? env.length : Object.keys(env as Record<string, unknown>).length;
    const volumes = config.volumes ?? {};

    return {
      name: info.name,
      status: info.status,
      image: config.image,
      cpus: config.cpus,
      memoryMib: config.memoryMib,
      envCount,
      volumeCount: Object.keys(volumes).length,
      createdAt: msToIsoString(info.createdAt),
      updatedAt: msToIsoString(info.updatedAt),
    };
  }

  private async readMsbInspect(name: string): Promise<MsbInspectEnvVolumes | undefined> {
    try {
      const { stdout } = await execAsync(
        `${getMsbPath()} inspect ${name} --format json`,
        { timeout: 5000 }
      );
      return parseMsbInspectEnvVolumes(stdout);
    } catch {
      return undefined;
    }
  }

  private async ensureNamedVolumes(volumes?: EnsureSandboxInput["volumes"]): Promise<void> {
    if (!volumes) {
      return;
    }

    const defaultQuotaMib = Number(process.env.MICROSANDBOX_VOLUME_QUOTA_MIB ?? "2048");

    const namedVolumes = Object.values(volumes).filter(
      (v): v is Extract<VolumeConfig, { type: "named" }> => v.type === "named"
    );

    await Promise.all(
      namedVolumes.map(async (definition) => {
        try {
          await Volume.get(definition.name);
        } catch {
          await Volume.builder(definition.name).quota(defaultQuotaMib).create();
        }
      })
    );
  }

  private async getOrEnsureNative(name: string): Promise<RuntimeNative> {
    return (await this.registry.ensure(name)) as RuntimeNative;
  }

  async ensureSandbox(
    name: string,
    input: EnsureSandboxInput
  ): Promise<{
    name: string;
    status: string;
  }> {
    const image = input.image ?? process.env.MICROSANDBOX_IMAGE ?? "python:3.11-slim";
    const cpus = input.cpus ?? Number(process.env.MICROSANDBOX_CPUS ?? "1");
    const memoryMib = input.memoryMib ?? Number(process.env.MICROSANDBOX_MEMORY ?? "512");

    await this.ensureNamedVolumes(input.volumes);

    await this.registry.ensure(name, {
      image,
      cpus,
      memoryMib,
      env: input.env,
      volumes: input.volumes,
    });

    return {
      name,
      status: "running",
    };
  }

  async startSandbox(name: string): Promise<{ name: string; status: string }> {
    await this.registry.ensure(name);
    return { name, status: "running" };
  }

  async stopSandbox(name: string): Promise<{ name: string; status: string }> {
    try {
      const native = await this.getOrEnsureNative(name);
      await native.stop();
      return { name, status: "stopped" };
    } catch (error) {
      if (error instanceof Error && error.message.includes("write to relay")) {
        return { name, status: "unknown" };
      }
      throw error;
    } finally {
      this.registry.remove(name);
    }
  }

  async killSandbox(name: string): Promise<{ name: string; status: string }> {
    try {
      const native = await this.getOrEnsureNative(name);
      await native.kill();
      return { name, status: "unknown" };
    } catch (error) {
      if (error instanceof Error && error.message.includes("write to relay")) {
        return { name, status: "unknown" };
      }
      throw error;
    } finally {
      this.registry.remove(name);
    }
  }

  async deleteSandbox(name: string): Promise<{ name: string; status: string }> {
    this.registry.remove(name);
    await this.registry.stopAndRemove(name);
    return { name, status: "unknown" };
  }

  async listSandboxes(_query: ListSandboxesQuery): Promise<{
    items: Array<{
      name: string;
      status: string;
      image: unknown;
      cpus?: number;
      memoryMib?: number;
      envCount: number;
      volumeCount: number;
      createdAt: string;
      updatedAt: string;
    }>;
    total: number;
  }> {
    const all: SandboxHandle[] = await Sandbox.list();

    const items = all
      .map((info) =>
        this.toListItem({
          name: info.name,
          status: info.status,
          configJson: info.configJson,
          createdAt: info.createdAt,
          updatedAt: info.updatedAt,
        })
      )
      .filter((item) => {
        if (_query.status && item.status !== _query.status) {
          return false;
        }

        if (_query.image && item.image !== _query.image) {
          return false;
        }

        if (_query.search) {
          const haystack = [item.name, item.image]
            .filter((value): value is string => Boolean(value))
            .join(" ")
            .toLowerCase();

          if (!haystack.includes(_query.search.toLowerCase())) {
            return false;
          }
        }

        return true;
      });

    return { items, total: items.length };
  }

  async getSandbox(
    name: string
  ): Promise<{
    name: string;
    status: string;
    image: unknown;
    cpus?: number;
    memoryMib?: number;
    env: unknown;
    volumes: Record<string, unknown>;
    metrics?: SandboxRuntimeMetrics;
    createdAt: string;
    updatedAt: string;
  } | undefined> {
    try {
      const handle: SandboxHandle = await Sandbox.get(name);
      const config = parseConfigJson(handle.configJson);

      const msbData = await this.readMsbInspect(name);
      const env = msbData?.env ?? config.env ?? {};
      const volumes = msbData?.volumes ?? config.volumes ?? {};

      let metrics: SandboxRuntimeMetrics | undefined;

      try {
        const m = await handle.metrics();
        metrics = {
          cpuPercent: m.cpuPercent,
          memoryBytes: m.memoryBytes,
          memoryLimitBytes: m.memoryLimitBytes,
          diskReadBytes: m.diskReadBytes,
          diskWriteBytes: m.diskWriteBytes,
          netRxBytes: m.netRxBytes,
          netTxBytes: m.netTxBytes,
          uptimeMs: m.uptimeMs,
          timestampMs: m.timestamp.getTime(),
        };
      } catch {
        metrics = undefined;
      }

      return {
        name: handle.name,
        status: handle.status,
        image: config.image,
        cpus: config.cpus,
        memoryMib: config.memoryMib,
        env,
        volumes,
        metrics,
        createdAt: msToIsoString(handle.createdAt),
        updatedAt: msToIsoString(handle.updatedAt),
      };
    } catch {
      return undefined;
    }
  }

  async getStatus(name: string): Promise<{ name: string; status: string }> {
    try {
      const handle: SandboxHandle = await Sandbox.get(name);
      return { name, status: handle.status };
    } catch {
      return { name, status: "unknown" };
    }
  }

  async readFile(sandboxName: string, path: string): Promise<{ path: string; content: string }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    const fs = native.fs();
    const content = typeof fs.readToString === "function"
      ? await fs.readToString(resolvedPath)
      : await fs.readString(resolvedPath);
    return { path: resolvedPath, content };
  }

  async writeFile(sandboxName: string, path: string, content: string): Promise<{ path: string }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    const parts = resolvedPath.split("/").filter(Boolean);
    for (let i = 1; i < parts.length; i++) {
      const dir = "/" + parts.slice(0, i).join("/");
      try {
        await native.fs().mkdir(dir);
      } catch {
        // directory may already exist or parent missing, continue
      }
    }
    await native.fs().write(resolvedPath, Buffer.from(content));
    return { path: resolvedPath };
  }

  async listPath(
    sandboxName: string,
    path: string,
    recursive?: boolean
  ): Promise<{ entries: Array<{ path: string; type: string }> }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    const entries = await native.fs().list(resolvedPath);
    return {
      entries: entries.map((entry) => ({
        path: entry.path,
        type: entry.kind,
      })),
    };
  }

  async findFiles(sandboxName: string, path: string, pattern: string): Promise<{ files: string[] }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    const matchFlag = pattern.includes("/") ? "-path" : "-name";
    const output = await native.exec("find", [resolvedPath, matchFlag, pattern, "-type", "f"]);
    return { files: output.stdout().split("\n").filter(Boolean) };
  }

  async searchInFile(
    sandboxName: string,
    path: string,
    query: string
  ): Promise<{ matches: Array<{ line: number; content: string }> }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    let stdout: string;

    try {
      const output = await native.exec("grep", ["-n", "-E", query, resolvedPath]);
      stdout = output.stdout();
    } catch (error) {
      if (typeof error === "object" && error !== null && "code" in error && error.code === 1) {
        return { matches: [] };
      }

      throw error;
    }

    return {
      matches: stdout
        .split("\n")
        .filter(Boolean)
        .map((line) => {
          const separator = line.indexOf(":");
          return {
            line: Number(line.slice(0, separator)),
            content: line.slice(separator + 1),
          };
        }),
    };
  }

  async replaceInFile(
    sandboxName: string,
    input: { path: string; search: string; replace: string }
  ): Promise<{ replaced: number }> {
    const resolvedPath = this.resolvePath(input.path);
    const native = await this.getOrEnsureNative(sandboxName);
    const fs = native.fs();
    const original = typeof fs.readToString === "function"
      ? await fs.readToString(resolvedPath)
      : await fs.readString(resolvedPath);

    if (!input.search) {
      return { replaced: 0 };
    }

    const occurrences = original.split(input.search).length - 1;
    if (occurrences === 0) {
      return { replaced: 0 };
    }

    const updated = original.split(input.search).join(input.replace);
    await fs.write(resolvedPath, Buffer.from(updated));

    return { replaced: occurrences };
  }

  async uploadFile(sandboxName: string, path: string, contentBase64: string): Promise<{ path: string }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    await native.fs().write(resolvedPath, Buffer.from(contentBase64, "base64"));
    return { path: resolvedPath };
  }

  async downloadFile(sandboxName: string, path: string): Promise<{ path: string; contentBase64: string }> {
    const resolvedPath = this.resolvePath(path);
    const native = await this.getOrEnsureNative(sandboxName);
    const data = await native.fs().read(resolvedPath);
    const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
    return { path: resolvedPath, contentBase64: buffer.toString("base64") };
  }

  async execCommand(input: ShellExecInput): Promise<{ stdout: string; stderr: string; exitCode: number }> {
    const native = await this.getOrEnsureNative(input.sandboxName);
    const output = await native.shell(input.command);

    return {
      stdout: output.stdout(),
      stderr: output.stderr(),
      exitCode: output.code ?? 0,
    };
  }

  async getSandboxLogs(
    name: string,
    opts: SandboxLogsInput
  ): Promise<{
    entries: Array<{
      timestamp: string;
      source: string;
      sessionId: number | null;
      text: string;
    }>;
  }> {
    const handle: SandboxHandle = await Sandbox.get(name);

    const logOpts: { tail?: number; since?: Date; until?: Date; sources?: readonly LogReadSource[] } = {};

    if (opts.tail !== undefined) {
      logOpts.tail = opts.tail;
    }

    if (opts.since) {
      logOpts.since = new Date(opts.since);
    }

    if (opts.until) {
      logOpts.until = new Date(opts.until);
    }

    if (opts.sources && opts.sources.length > 0) {
      logOpts.sources = opts.sources as LogReadSource[];
    } else {
      logOpts.sources = ["all" as LogReadSource];
    }

    const entries = await handle.logs(logOpts);

    return {
      entries: entries.map((entry) => ({
        timestamp: entry.timestamp.toISOString(),
        source: entry.source,
        sessionId: entry.sessionId,
        text: entry.text(),
      })),
    };
  }
}
