import type { OpencodeClient } from '@opencode-ai/sdk';
import { createOpencodeClient } from '@opencode-ai/sdk';

import type { NormalizedAgentEvent } from './agent-events';
import { normalizeOpenCodeEvent } from './agent-events';

export type OpenCodeSession = {
  client: OpencodeClient;
  sessionId: string;
  close: () => void;
};

const basicAuth = (username: string, password: string) =>
  `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;

const modelParts = (model: string) => {
  const [rawProviderId, ...rest] = model.split('/');
  const providerID =
    rawProviderId && rawProviderId.length > 0 ? rawProviderId : 'openai';
  const modelID = rest.length > 0 ? rest.join('/') : model;
  return {
    providerID,
    modelID,
  };
};

export const createOpenCodeSession = async (args: {
  baseUrl: string;
  password: string;
  directory: string;
  title: string;
  onEvent: (event: NormalizedAgentEvent) => Promise<void>;
}) => {
  const abortController = new AbortController();
  const client = createOpencodeClient({
    baseUrl: args.baseUrl,
    directory: args.directory,
    headers: {
      authorization: basicAuth('opencode', args.password),
    },
  });
  const created = await client.session.create({
    query: { directory: args.directory },
    body: { title: args.title },
  });
  if (!created.data) {
    throw new Error('OpenCode session could not be created.');
  }
  const sessionId = created.data.id;
  void (async () => {
    const events = await client.event.subscribe({
      signal: abortController.signal,
      query: { directory: args.directory },
      onSseEvent: (event) => {
        for (const normalized of normalizeOpenCodeEvent(event.data)) {
          void args.onEvent(normalized);
        }
      },
      onSseError: (error) => {
        void args.onEvent({
          kind: 'error',
          message: error instanceof Error ? error.message : String(error),
        });
      },
    });
    for await (const event of events.stream) {
      for (const normalized of normalizeOpenCodeEvent(event)) {
        await args.onEvent(normalized);
      }
    }
  })().catch((error: unknown) => {
    if (!abortController.signal.aborted) {
      void args.onEvent({
        kind: 'error',
        message: error instanceof Error ? error.message : String(error),
      });
    }
  });
  return {
    client,
    sessionId,
    close: () => abortController.abort(),
  } satisfies OpenCodeSession;
};

export const promptOpenCodeSession = async (args: {
  session: OpenCodeSession;
  prompt: string;
  model: string;
  directory: string;
}) => {
  const model = modelParts(args.model);
  const result = await args.session.client.session.promptAsync({
    path: { id: args.session.sessionId },
    query: { directory: args.directory },
    body: {
      model,
      parts: [{ type: 'text', text: args.prompt }],
    },
  });
  if (result.error) {
    throw new Error('OpenCode prompt was rejected.');
  }
};

export const abortOpenCodeSession = async (session: OpenCodeSession) => {
  await session.client.session.abort({
    path: { id: session.sessionId },
  });
};

export const replyOpenCodePermission = async (args: {
  session: OpenCodeSession;
  permissionId: string;
  response: 'once' | 'always' | 'reject';
  directory: string;
}) => {
  const result = await args.session.client.postSessionIdPermissionsPermissionId(
    {
      path: { id: args.session.sessionId, permissionID: args.permissionId },
      query: { directory: args.directory },
      body: { response: args.response },
    },
  );
  if (result.error) {
    throw new Error('OpenCode permission response was rejected.');
  }
};
