import { randomUUID } from "node:crypto";

import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import {
  removeRuntimeSessionContext,
  setRuntimeSessionContext,
} from "../runtime/sessionContext.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";

export type SessionBinding = {
  clientSessionId: string;
  sessionId: string;
  startedAt: number;
};

export type SessionContext = {
  clientSessionId: string;
  accessToken?: string;
  network?: string;
  projectId?: string;
  tokenExpiresAt?: string;
  userId?: string;
};

export type ChatRequestContext = SessionContext & {
  actorKey: string;
  projectKey: string;
  traceId: string;
};

export class ChatSessionBridge {
  private readonly abortControllers = new Map<string, AbortController>();

  constructor(private readonly runtime: OpencodeRuntimeAdapter) {}

  async resolve(context: {
    sessionId?: string;
    accessToken?: string;
    network?: string;
    projectId?: string;
    traceId?: string;
    tokenExpiresAt?: string;
    userId?: string;
  }): Promise<{
    binding: SessionBinding;
    requestContext: ChatRequestContext;
    created: boolean;
  }> {
    let requestContext = this.buildRequestContext(context);
    const existingSessionId = context.sessionId?.trim();
    await this.abortActiveRuntime(requestContext.clientSessionId, existingSessionId);

    let sessionId = existingSessionId;
    let created = false;
    if (!sessionId) {
      const session = await this.runtime.createSession();
      sessionId = session.id;
      requestContext = {
        ...requestContext,
        clientSessionId: sessionId,
      };
      created = true;
    }
    const binding: SessionBinding = {
      clientSessionId: requestContext.clientSessionId,
      sessionId,
      startedAt: Date.now(),
    };
    setRuntimeSessionContext({
      accessToken: requestContext.accessToken,
      actorKey: requestContext.actorKey,
      allowLearningWrite: true,
      clientSessionId: requestContext.clientSessionId,
      learningMode: "interactive",
      network: requestContext.network,
      projectId: requestContext.projectId,
      projectKey: requestContext.projectKey,
      sessionId,
      tokenExpiresAt: requestContext.tokenExpiresAt,
      traceId: requestContext.traceId,
    });

    return { binding, requestContext, created };
  }

  count(): number {
    return this.abortControllers.size;
  }

  createClientSessionId() {
    return `agent-${randomUUID().slice(0, 12)}`;
  }

  registerAbortController(clientSessionId: string, controller: AbortController) {
    this.abortControllers.set(clientSessionId, controller);
  }

  finalizeRequest(clientSessionId: string) {
    this.abortControllers.delete(clientSessionId);
  }

  async abort(context: {
    clientSessionId?: string;
    sessionId?: string;
  }): Promise<SessionBinding | null> {
    const clientSessionId = context.clientSessionId?.trim();
    const sessionId = context.sessionId?.trim();
    if (!clientSessionId || !sessionId) {
      return null;
    }

    await this.abortActiveRuntime(clientSessionId, sessionId);
    return {
      clientSessionId,
      sessionId,
      startedAt: Date.now(),
    };
  }

  async deleteSession(context: {
    clientSessionId: string;
    sessionId: string;
  }) {
    const clientSessionId = context.clientSessionId.trim();
    const sessionId = context.sessionId.trim();
    const controller = this.abortControllers.get(clientSessionId);
    if (controller) {
      this.abortControllers.delete(clientSessionId);
      controller.abort();
    }
    await this.runtime.abortSession(sessionId).catch((error) => {
      logger.warn(
        { clientSessionId, sessionId, err: error },
        "failed to abort runtime session",
      );
    });
    await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
      logger.warn(
        { clientSessionId, sessionId, err: error },
        "failed while waiting for runtime session to become idle",
      );
    });
    removeRuntimeSessionContext(sessionId);
  }

  private buildRequestContext(context: {
    sessionId?: string;
    accessToken?: string;
    network?: string;
    projectId?: string;
    traceId?: string;
    tokenExpiresAt?: string;
    userId?: string;
  }): ChatRequestContext {
    const sessionId = context.sessionId?.trim();
    return {
      clientSessionId: sessionId || this.createClientSessionId(),
      accessToken: context.accessToken,
      actorKey: toActorKey(context.userId),
      network: context.network,
      projectId: context.projectId,
      projectKey: toProjectKey(context.projectId),
      tokenExpiresAt: context.tokenExpiresAt,
      traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
      userId: context.userId?.trim(),
    };
  }

  private async abortActiveRuntime(clientSessionId: string, sessionId?: string) {
    const controller = this.abortControllers.get(clientSessionId);
    if (controller) {
      this.abortControllers.delete(clientSessionId);
      controller.abort();
    }

    if (!sessionId) {
      return;
    }
    await this.runtime.abortSession(sessionId).catch((error) => {
      logger.warn(
        { clientSessionId, sessionId, err: error },
        "failed to abort active runtime session",
      );
    });
    await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
      logger.warn(
        { clientSessionId, sessionId, err: error },
        "failed while waiting for active runtime session to become idle",
      );
    });
  }
}
