import { randomUUID } from 'node:crypto';
import type { EventService } from '../events/service.js';
import type { CreateSandboxRecord, SandboxRecord, SandboxStore } from '../store/types.js';
import { sandboxRuntimeId, withNewSandboxRuntime, withSandboxRuntimeMetadata } from './runtime.js';
import { SandboxProviderUnavailableError, type SandboxHandle, type SandboxProvider } from './types.js';

export type EnsureSandboxResult = {
  sandbox: SandboxHandle;
  record: SandboxRecord;
  created: boolean;
  restarted: boolean;
};

export type EnsureSandboxOptions = {
  allowCreate?: boolean;
};

export class SandboxLifecycleUnavailableError extends Error {}

export class SandboxLifecycleService {
  private readonly localLifecycleLocks = new Map<string, Promise<void>>();

  constructor(
    private readonly store: SandboxStore,
    private readonly provider: SandboxProvider,
  ) {}

  async ensure(sessionId: string): Promise<EnsureSandboxResult>;
  async ensure(
    sessionId: string,
    options: EnsureSandboxOptions & { allowCreate: false },
  ): Promise<EnsureSandboxResult | null>;
  async ensure(sessionId: string, options: EnsureSandboxOptions = {}): Promise<EnsureSandboxResult | null> {
    return this.withLifecycleLock(sessionId, () => this.ensureUnlocked(sessionId, options));
  }

  private async ensureUnlocked(sessionId: string, options: EnsureSandboxOptions): Promise<EnsureSandboxResult | null> {
    const existing = await this.store.getActiveSandbox(sessionId, this.provider.name);
    if (existing) {
      const connected = await this.tryConnect(existing, options);
      if (connected) return { ...connected, created: false };
    }
    if (options.allowCreate === false) return null;

    const sandbox = await this.provider.create({ sessionId });
    let record: SandboxRecord | undefined;
    try {
      const now = new Date();
      const metadata = withSandboxRuntimeMetadata(sandbox.metadata);
      const createRecord: CreateSandboxRecord = {
        id: randomUUID(),
        sessionId,
        provider: this.provider.name,
        providerSandboxId: sandbox.providerSandboxId,
        status: 'ready',
        workspacePath: sandbox.workspacePath,
        metadata,
        createdAt: now,
        updatedAt: now,
      };
      record = sandbox.secrets
        ? await this.store.createSandboxWithSecrets(createRecord, sandbox.secrets)
        : await this.store.createSandbox(createRecord);
    } catch (error) {
      await this.provider.destroy(sandbox).catch(() => undefined);
      if (record) {
        const destroyedAt = new Date();
        await this.store
          .updateSandbox({ ...record, status: 'destroyed', updatedAt: destroyedAt, destroyedAt })
          .catch(() => undefined);
      }
      throw error;
    }

    return { sandbox: { ...sandbox, metadata: record.metadata }, record, created: true, restarted: false };
  }

  private async withLifecycleLock<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
    const key = `${this.provider.name}:${sessionId}`;
    const previous = this.localLifecycleLocks.get(key) ?? Promise.resolve();
    let release!: () => void;
    const current = new Promise<void>((resolve) => {
      release = resolve;
    });
    const queued = previous.then(() => current);
    this.localLifecycleLocks.set(key, queued);
    await previous;
    try {
      return await this.store.withSandboxLifecycleLock(sessionId, this.provider.name, operation);
    } finally {
      release();
      if (this.localLifecycleLocks.get(key) === queued) this.localLifecycleLocks.delete(key);
    }
  }

  private async tryConnect(
    record: SandboxRecord,
    options: EnsureSandboxOptions,
  ): Promise<Omit<EnsureSandboxResult, 'created'> | null> {
    const checkedAt = new Date();
    let health;
    try {
      health = await this.provider.health(record);
    } catch (cause) {
      if (!(cause instanceof SandboxProviderUnavailableError)) throw cause;
      throw new SandboxLifecycleUnavailableError('Sandbox health check failed', { cause });
    }
    let restarted = record.status !== 'ready' || health.status === 'stopped' || health.status === 'starting';
    const checkedRecord: SandboxRecord = {
      ...record,
      status: health.status === 'ready' ? 'ready' : health.status === 'stopped' ? 'stopped' : 'unhealthy',
      lastHealthCheckAt: checkedAt,
      updatedAt: checkedAt,
    };
    await this.store.updateSandbox(checkedRecord);

    if (health.status === 'stopped') {
      if (!this.provider.start) return null;
      try {
        await this.provider.start(record);
      } catch (cause) {
        if (!(cause instanceof SandboxProviderUnavailableError)) throw cause;
        throw new SandboxLifecycleUnavailableError('Sandbox resume failed', { cause });
      }
      restarted = true;
    } else if (health.status !== 'ready' && !(health.status === 'starting' && options.allowCreate === false)) {
      return null;
    }

    const secrets = await this.getSandboxSecretsForReconnect(record, checkedRecord, options);
    if (!secrets) return null;
    let sandbox: SandboxHandle;
    try {
      sandbox = await this.provider.connect({
        providerSandboxId: record.providerSandboxId,
        sessionId: record.sessionId,
        metadata: record.metadata,
        secrets,
      });
    } catch (cause) {
      if (!(cause instanceof SandboxProviderUnavailableError)) throw cause;
      await this.store.updateSandbox({
        ...checkedRecord,
        status: 'unhealthy',
        updatedAt: new Date(),
      });
      return null;
    }

    if (
      sandbox.provider !== this.provider.name ||
      sandbox.providerSandboxId !== record.providerSandboxId ||
      sandbox.sessionId !== record.sessionId
    )
      throw new Error(`Sandbox identity changed while reconnecting ${record.providerSandboxId}`);
    const current = await this.store.getActiveSandbox(record.sessionId, this.provider.name);
    if (
      !current ||
      current.id !== record.id ||
      current.providerSandboxId !== record.providerSandboxId ||
      sandboxRuntimeId(current) !== sandboxRuntimeId(checkedRecord)
    )
      throw new Error(`Sandbox record changed while reconnecting ${record.providerSandboxId}`);
    if (sandbox.secrets) await this.store.setSandboxSecrets(record.id, sandbox.secrets);
    const baseRecord = restarted ? withNewSandboxRuntime(current) : current;
    const updated = await this.store.updateSandbox({
      ...baseRecord,
      status: 'ready',
      workspacePath: sandbox.workspacePath,
      metadata: { ...sandbox.metadata, ...baseRecord.metadata },
      updatedAt: new Date(),
    });
    return { sandbox: { ...sandbox, metadata: updated.metadata }, record: updated, restarted };
  }

  private async getSandboxSecretsForReconnect(
    record: SandboxRecord,
    checkedRecord: SandboxRecord,
    options: EnsureSandboxOptions,
  ): Promise<Record<string, string> | null> {
    try {
      return await this.store.getSandboxSecrets(record.id);
    } catch (error) {
      if (options.allowCreate === false) throw error;
      await this.provider.destroy(record).catch(() => undefined);
      const destroyedAt = new Date();
      await this.store
        .updateSandbox({ ...checkedRecord, status: 'destroyed', updatedAt: destroyedAt, destroyedAt })
        .catch(() => undefined);
      console.warn(
        `Destroyed ${this.provider.name} sandbox ${record.providerSandboxId} after failing to load sandbox secrets: ${
          error instanceof Error ? error.message : 'unknown error'
        }`,
      );
      return null;
    }
  }
}

export type SandboxCleanupResult = {
  destroyed: number;
  stopped: number;
  failed: number;
};

export type SandboxKeepaliveResult = {
  record: SandboxRecord;
  keepaliveUntil: Date;
  providerSync: 'not_supported' | 'ok' | 'failed';
};

export class SandboxKeepaliveService {
  constructor(
    private readonly store: SandboxStore,
    private readonly events: EventService,
    private readonly provider: SandboxProvider,
  ) {}

  async extend(input: {
    sessionId: string;
    durationMs: number;
    maxDurationMs: number;
    port?: number;
  }): Promise<SandboxKeepaliveResult | null> {
    return this.store.withSandboxLifecycleLock(input.sessionId, this.provider.name, async () => {
      const sandbox = await this.store.getActiveSandbox(input.sessionId, this.provider.name);
      if (!sandbox || sandbox.status !== 'ready') return null;

      const durationMs = input.durationMs;
      const now = new Date();
      const baseTime =
        sandbox.keepaliveUntil && sandbox.keepaliveUntil > now ? sandbox.keepaliveUntil.getTime() : now.getTime();
      const maxUntilMs = now.getTime() + input.maxDurationMs;
      const requestedUntilMs = baseTime + durationMs;
      const keepaliveUntil = new Date(Math.min(requestedUntilMs, maxUntilMs));
      const updated = await this.store.updateSandbox({
        ...sandbox,
        keepaliveUntil,
        updatedAt: now,
      });
      const providerSync = await this.syncProviderKeepalive(updated, durationMs);
      await this.events.append({
        sessionId: sandbox.sessionId,
        type: 'sandbox_keepalive_extended',
        payload: {
          reason: 'manual_extend',
          provider: sandbox.provider,
          providerSandboxId: sandbox.providerSandboxId,
          keepaliveUntil: updated.keepaliveUntil!.toISOString(),
          extendedBySeconds: Math.ceil(durationMs / 1000),
          providerSync,
          ...(input.port ? { port: input.port } : {}),
        },
      });
      return { record: updated, keepaliveUntil: updated.keepaliveUntil!, providerSync };
    });
  }

  private async syncProviderKeepalive(
    record: SandboxRecord,
    durationMs: number,
  ): Promise<SandboxKeepaliveResult['providerSync']> {
    if (!this.provider.refreshKeepalive) return 'not_supported';
    try {
      await this.provider.refreshKeepalive({
        providerSandboxId: record.providerSandboxId,
        sessionId: record.sessionId,
        durationMs,
      });
      return 'ok';
    } catch {
      return 'failed';
    }
  }
}

export class SandboxCleanupService {
  constructor(
    private readonly store: SandboxStore,
    private readonly events: EventService,
    private readonly provider: SandboxProvider,
  ) {}

  async destroySessionSandboxes(sessionId: string): Promise<SandboxCleanupResult> {
    const sandboxes = await this.store.listActiveSandboxes(sessionId, this.provider.name);
    const result = await this.destroySandboxes(sandboxes, 'archive', { respectKeepalive: false });
    return { ...result, stopped: 0 };
  }

  async destroyIdleSandboxes(input: { idleBefore: Date; limit: number }): Promise<SandboxCleanupResult> {
    const sandboxes = await this.store.listIdleSandboxes({
      provider: this.provider.name,
      idleBefore: input.idleBefore,
      limit: input.limit,
    });
    const result = await this.destroySandboxes(sandboxes, 'idle_reaper', { respectKeepalive: true });
    return { ...result, stopped: 0 };
  }

  async stopIdleSandboxes(input: { idleBefore: Date; limit: number }): Promise<SandboxCleanupResult> {
    if (!this.provider.stop) return { destroyed: 0, stopped: 0, failed: 0 };
    const sandboxes = await this.store.listStoppableSandboxes({
      provider: this.provider.name,
      idleBefore: input.idleBefore,
      limit: input.limit,
    });
    return this.stopSandboxes(sandboxes, 'idle_stop');
  }

  private async stopSandboxes(sandboxes: SandboxRecord[], reason: string): Promise<SandboxCleanupResult> {
    let stopped = 0;
    let failed = 0;

    for (const sandbox of sandboxes) {
      try {
        const didStop = await this.store.withSandboxLifecycleLock(sandbox.sessionId, sandbox.provider, async () => {
          const current = await this.currentActiveSandbox(sandbox);
          if (!current || isKeepaliveActive(current)) return false;
          await this.provider.stop?.(current);
          const stoppedAt = new Date();
          await this.store.updateSandbox({ ...current, status: 'stopped', updatedAt: stoppedAt });
          await this.events.append({
            sessionId: current.sessionId,
            type: 'sandbox_stopped',
            payload: { reason, provider: current.provider, providerSandboxId: current.providerSandboxId },
          });
          return true;
        });
        if (didStop) stopped += 1;
      } catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown sandbox stop error';
        await this.events.append({
          sessionId: sandbox.sessionId,
          type: 'sandbox_stop_failed',
          payload: { reason, provider: sandbox.provider, providerSandboxId: sandbox.providerSandboxId, error: message },
        });
        failed += 1;
      }
    }

    return { destroyed: 0, stopped, failed };
  }

  private async destroySandboxes(
    sandboxes: SandboxRecord[],
    reason: string,
    options: { respectKeepalive: boolean },
  ): Promise<Omit<SandboxCleanupResult, 'stopped'>> {
    let destroyed = 0;
    let failed = 0;

    for (const sandbox of sandboxes) {
      try {
        const didDestroy = await this.store.withSandboxLifecycleLock(sandbox.sessionId, sandbox.provider, async () => {
          const current = await this.currentActiveSandbox(sandbox);
          if (!current || (options.respectKeepalive && isKeepaliveActive(current))) return false;
          await this.provider.destroy(current);
          const destroyedAt = new Date();
          await this.store.updateSandbox({
            ...current,
            status: 'destroyed',
            updatedAt: destroyedAt,
            destroyedAt,
          });
          await this.events.append({
            sessionId: current.sessionId,
            type: 'sandbox_destroyed',
            payload: {
              reason,
              provider: current.provider,
              providerSandboxId: current.providerSandboxId,
            },
          });
          return true;
        });
        if (didDestroy) destroyed += 1;
      } catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown sandbox destroy error';
        await this.events.append({
          sessionId: sandbox.sessionId,
          type: 'sandbox_destroy_failed',
          payload: {
            reason,
            provider: sandbox.provider,
            providerSandboxId: sandbox.providerSandboxId,
            error: message,
          },
        });
        failed += 1;
      }
    }

    return { destroyed, failed };
  }

  private async currentActiveSandbox(sandbox: SandboxRecord): Promise<SandboxRecord | null> {
    const current = (await this.store.listActiveSandboxes(sandbox.sessionId, sandbox.provider)).find(
      (candidate) => candidate.id === sandbox.id,
    );
    return current ?? null;
  }
}

function isKeepaliveActive(sandbox: SandboxRecord): boolean {
  return Boolean(sandbox.keepaliveUntil && sandbox.keepaliveUntil > new Date());
}
