import { createRequire } from 'module';
import { pathToFileURL } from 'url';
import type { ExternalBackend, ExternalBackendRequest } from './external.js';

const DEFAULT_TIMEOUT_MS = 120_000;
const DEFAULT_MODULE_CANDIDATES = [
  '@mariozechner/pi-agent-core',
  '@badlogic/pi-agent-core',
  '@openclaw/pi-agent-core',
  'pi-agent-core',
] as const;
const PI_AI_MODULE_CANDIDATES = [
  '@mariozechner/pi-ai',
  '@badlogic/pi-ai',
  '@openclaw/pi-ai',
  'pi-ai',
] as const;

type PiSystemPromptMode = 'flynn' | 'pi_default' | 'hybrid';

interface PiModuleLike {
  [key: string]: unknown;
}

interface PiSessionLike {
  [key: string]: unknown;
}

interface PiModelFactoryModuleLike extends PiModuleLike {
  getModel?: (provider: string, modelId: string) => unknown;
}

export interface PiEmbeddedBackendOptions {
  timeoutMs?: number;
  model?: string;
  defaultModelSpec?: string;
  getApiKey?: (provider: string) => string | undefined | Promise<string | undefined>;
  systemPromptMode?: PiSystemPromptMode;
  module?: string;
}

function buildPrompt(request: ExternalBackendRequest): string {
  const lines: string[] = [];
  if (request.systemPrompt?.trim()) {
    lines.push(`SYSTEM: ${request.systemPrompt.trim()}`);
  }
  for (const item of request.history) {
    if (!item.content.trim()) {
      continue;
    }
    lines.push(`${item.role.toUpperCase()}: ${item.content}`);
  }
  lines.push(`USER: ${request.prompt}`);
  return lines.join('\n\n');
}

function isModuleNotFound(error: unknown, moduleName: string): boolean {
  if (!(error instanceof Error)) {
    return false;
  }
  const code = (error as NodeJS.ErrnoException).code;
  if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
    return true;
  }
  return (
    error.message.includes(`Cannot find package '${moduleName}'`)
    || error.message.includes(`Cannot find module '${moduleName}'`)
  );
}

function toErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function withTimeout<T>(promise: Promise<T>, timeoutMs: number, description: string): Promise<T> {
  let timeoutHandle: NodeJS.Timeout | undefined;
  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutHandle = setTimeout(() => {
      reject(new Error(`${description} timed out after ${timeoutMs}ms`));
    }, timeoutMs);
  });

  return Promise.race([promise, timeoutPromise])
    .finally(() => {
      if (timeoutHandle) {
        clearTimeout(timeoutHandle);
      }
    });
}

function getSessionFactory(moduleLike: PiModuleLike): ((args: Record<string, unknown>) => unknown) | undefined {
  const candidateNames = [
    'createAgentSession',
    'createSession',
    'createPiSession',
    'createAgent',
  ];
  for (const name of candidateNames) {
    const candidate = moduleLike[name];
    if (typeof candidate === 'function') {
      return candidate as (args: Record<string, unknown>) => unknown;
    }
  }
  return undefined;
}

function getAgentConstructor(moduleLike: PiModuleLike): (new (options?: Record<string, unknown>) => unknown) | undefined {
  const direct = moduleLike.Agent;
  if (typeof direct === 'function') {
    return direct as new (options?: Record<string, unknown>) => unknown;
  }

  const defaultExport = moduleLike.default;
  if (defaultExport && typeof defaultExport === 'object') {
    const nested = (defaultExport as PiModuleLike).Agent;
    if (typeof nested === 'function') {
      return nested as new (options?: Record<string, unknown>) => unknown;
    }
  }
  return undefined;
}

function parseModelSpec(raw: string): { provider: string; modelId: string } {
  const spec = raw.trim();
  const separator = spec.includes(':') ? ':' : spec.includes('/') ? '/' : undefined;
  if (!separator) {
    throw new Error(
      `Invalid pi_embedded model "${raw}". Use "<provider>:<model>" (for example "openai:gpt-5.2").`,
    );
  }
  const index = spec.indexOf(separator);
  const provider = spec.slice(0, index).trim();
  const modelId = spec.slice(index + 1).trim();
  if (!provider || !modelId) {
    throw new Error(
      `Invalid pi_embedded model "${raw}". Use "<provider>:<model>" (for example "openai:gpt-5.2").`,
    );
  }
  return { provider, modelId };
}

function isPackageSpecifier(moduleName: string): boolean {
  return !(moduleName.startsWith('.') || moduleName.startsWith('/') || moduleName.startsWith('file:'));
}

function extractText(value: unknown): string | undefined {
  if (typeof value === 'string') {
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : undefined;
  }
  if (!value || typeof value !== 'object') {
    return undefined;
  }

  const obj = value as Record<string, unknown>;
  const scalarKeys = ['text', 'output', 'response', 'message', 'content'];
  for (const key of scalarKeys) {
    const scalar = obj[key];
    if (typeof scalar === 'string') {
      const trimmed = scalar.trim();
      if (trimmed.length > 0) {
        return trimmed;
      }
    }
  }

  const content = obj.content;
  if (Array.isArray(content)) {
    const textParts = content
      .map((part) => {
        if (!part || typeof part !== 'object') {
          return '';
        }
        const textValue = (part as Record<string, unknown>).text;
        return typeof textValue === 'string' ? textValue.trim() : '';
      })
      .filter((part) => part.length > 0);
    if (textParts.length > 0) {
      return textParts.join('\n');
    }
  }

  return undefined;
}

async function maybeDisposeSession(session: unknown): Promise<void> {
  if (!session || typeof session !== 'object') {
    return;
  }
  const obj = session as PiSessionLike;
  const disposeMethods = ['close', 'dispose', 'destroy'];
  for (const methodName of disposeMethods) {
    const method = obj[methodName];
    if (typeof method === 'function') {
      await Promise.resolve(method.call(session));
      return;
    }
  }
}

export class PiEmbeddedBackend implements ExternalBackend {
  readonly name = 'pi_embedded' as const;
  private readonly timeoutMs: number;
  private readonly model?: string;
  private readonly defaultModelSpec?: string;
  private readonly getApiKey?: (provider: string) => string | undefined | Promise<string | undefined>;
  private readonly systemPromptMode: PiSystemPromptMode;
  private readonly moduleOverride?: string;

  constructor(options: PiEmbeddedBackendOptions = {}) {
    this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    this.model = options.model;
    this.defaultModelSpec = options.defaultModelSpec;
    this.getApiKey = options.getApiKey;
    this.systemPromptMode = options.systemPromptMode ?? 'hybrid';
    this.moduleOverride = options.module;
  }

  async process(input: ExternalBackendRequest): Promise<string> {
    const prompt = buildPrompt(input);
    const { moduleLike, moduleName } = await this.loadPiModule();
    const factory = getSessionFactory(moduleLike);
    const effectiveSystemPrompt = this.systemPromptMode !== 'pi_default'
      ? input.systemPrompt?.trim() || undefined
      : undefined;
    const requestPayload: Record<string, unknown> = {
      prompt,
      input: input.prompt,
      history: input.history,
      messages: [
        ...input.history.map((entry) => ({ role: entry.role, content: entry.content })),
        { role: 'user', content: input.prompt },
      ],
      ...(this.model ? { model: this.model } : {}),
      systemPromptMode: this.systemPromptMode,
      ...(effectiveSystemPrompt ? {
        system: effectiveSystemPrompt,
        systemPrompt: effectiveSystemPrompt,
      } : {}),
    };

    if (factory) {
      const session = await withTimeout(
        Promise.resolve(factory(requestPayload)),
        this.timeoutMs,
        'Pi embedded session initialization',
      );

      try {
        const response = await withTimeout(
          this.invokeSession(session, requestPayload, prompt),
          this.timeoutMs,
          'Pi embedded request',
        );
        const text = extractText(response);
        if (!text) {
          throw new Error('Pi embedded backend returned no text output');
        }
        return text;
      } finally {
        await maybeDisposeSession(session);
      }
    }

    const AgentCtor = getAgentConstructor(moduleLike);
    if (!AgentCtor) {
      throw new Error(
        'Loaded Pi module does not expose a supported runtime API ' +
        '(expected one of: createAgentSession/createSession/createPiSession/createAgent, or Agent class export)',
      );
    }
    return withTimeout(
      this.invokeAgentRuntime(AgentCtor, input, moduleName, effectiveSystemPrompt),
      this.timeoutMs,
      'Pi embedded request',
    );
  }

  private async loadPiModule(): Promise<{ moduleLike: PiModuleLike; moduleName: string }> {
    const candidates = this.moduleOverride
      ? [this.moduleOverride]
      : [...DEFAULT_MODULE_CANDIDATES];
    const failures: string[] = [];

    for (const moduleName of candidates) {
      try {
        const loaded = await import(moduleName);
        return {
          moduleLike: loaded as PiModuleLike,
          moduleName,
        };
      } catch (error) {
        if (isModuleNotFound(error, moduleName)) {
          failures.push(`${moduleName}: not installed`);
          continue;
        }
        failures.push(`${moduleName}: ${toErrorMessage(error)}`);
      }
    }

    throw new Error(
      'Failed to load Pi embedded runtime module. ' +
      `Tried: ${candidates.join(', ')}. ` +
      `Details: ${failures.join(' | ')}`,
    );
  }

  private async invokeAgentRuntime(
    AgentCtor: new (options?: Record<string, unknown>) => unknown,
    input: ExternalBackendRequest,
    moduleName: string,
    systemPrompt?: string,
  ): Promise<string> {
    const modelSpec = this.model ?? this.defaultModelSpec;
    const model = modelSpec
      ? await this.resolvePiModel(modelSpec, moduleName)
      : undefined;

    const agentOptions: Record<string, unknown> = {};
    if (model) {
      agentOptions.initialState = { model };
    }
    if (this.getApiKey) {
      agentOptions.getApiKey = this.getApiKey;
    }
    if (systemPrompt) {
      agentOptions.systemPrompt = systemPrompt;
      agentOptions.system = systemPrompt;
    }

    const agent = Object.keys(agentOptions).length > 0
      ? new AgentCtor(agentOptions)
      : new AgentCtor();
    if (!agent || typeof agent !== 'object') {
      throw new Error('Pi Agent constructor returned an invalid object');
    }
    const agentObj = agent as PiSessionLike;

    if (systemPrompt) {
      const setSystemPrompt = agentObj.setSystemPrompt;
      if (typeof setSystemPrompt === 'function') {
        await Promise.resolve(setSystemPrompt.call(agent, systemPrompt));
      }
    }

    if (model) {
      const setModel = agentObj.setModel;
      if (typeof setModel === 'function') {
        await Promise.resolve(setModel.call(agent, model));
      }
    }

    const replaceMessages = agentObj.replaceMessages;
    if (typeof replaceMessages === 'function') {
      const historyMessages = input.history.map((entry, index) => ({
        role: entry.role,
        content: [{ type: 'text', text: entry.content }],
        timestamp: Date.now() + index,
      }));
      await Promise.resolve(replaceMessages.call(agent, historyMessages));
    }

    const promptMethod = agentObj.prompt;
    if (typeof promptMethod !== 'function') {
      throw new Error('Pi Agent runtime does not expose prompt()');
    }
    await Promise.resolve(promptMethod.call(agent, input.prompt));

    const state = agentObj.state;
    if (!state || typeof state !== 'object') {
      throw new Error('Pi Agent runtime returned no state after prompt()');
    }
    const messages = (state as PiSessionLike).messages;
    if (!Array.isArray(messages)) {
      throw new Error('Pi Agent runtime state does not include messages');
    }

    for (let index = messages.length - 1; index >= 0; index -= 1) {
      const message = messages[index];
      if (!message || typeof message !== 'object') {
        continue;
      }
      const role = (message as PiSessionLike).role;
      if (role !== 'assistant') {
        continue;
      }
      const text = extractText(message);
      if (text) {
        return text;
      }
    }

    const stateError = (state as PiSessionLike).error;
    if (typeof stateError === 'string' && stateError.trim().length > 0) {
      throw new Error(`Pi Agent runtime produced no assistant text: ${stateError}`);
    }
    throw new Error('Pi Agent runtime produced no assistant text');
  }

  private async resolvePiModel(modelSpec: string, moduleName: string): Promise<unknown> {
    const { provider, modelId } = parseModelSpec(modelSpec);
    const piAi = await this.loadPiAiModule(moduleName);
    const getModel = piAi.getModel;
    if (typeof getModel !== 'function') {
      throw new Error('Pi AI module does not expose getModel() required for pi_embedded.model override');
    }
    try {
      return getModel(provider, modelId);
    } catch (error) {
      throw new Error(
        `Failed to resolve pi_embedded.model "${modelSpec}": ${toErrorMessage(error)}`,
      );
    }
  }

  private async loadPiAiModule(moduleName: string): Promise<PiModelFactoryModuleLike> {
    const rootRequire = createRequire(import.meta.url);
    let resolver = rootRequire;

    if (isPackageSpecifier(moduleName)) {
      try {
        const modulePackageJson = rootRequire.resolve(`${moduleName}/package.json`);
        resolver = createRequire(modulePackageJson);
      } catch {
        resolver = rootRequire;
      }
    }

    const failures: string[] = [];
    for (const candidate of PI_AI_MODULE_CANDIDATES) {
      try {
        const resolvedEntry = resolver.resolve(candidate);
        const loaded = await import(pathToFileURL(resolvedEntry).href);
        return loaded as PiModelFactoryModuleLike;
      } catch (error) {
        failures.push(`${candidate}: ${toErrorMessage(error)}`);
      }
    }
    throw new Error(
      'Failed to load Pi AI model registry module. ' +
      `Tried: ${PI_AI_MODULE_CANDIDATES.join(', ')}. ` +
      `Details: ${failures.join(' | ')}`,
    );
  }

  private async invokeSession(
    session: unknown,
    requestPayload: Record<string, unknown>,
    prompt: string,
  ): Promise<unknown> {
    if (typeof session === 'function') {
      return Promise.resolve(session(requestPayload));
    }
    if (!session || typeof session !== 'object') {
      throw new Error('Pi session factory returned an invalid session object');
    }

    const sessionObj = session as PiSessionLike;
    const methodNames = ['run', 'sendMessage', 'chat', 'complete', 'invoke', 'process'];
    for (const methodName of methodNames) {
      const method = sessionObj[methodName];
      if (typeof method !== 'function') {
        continue;
      }

      const payloadAttempts: unknown[] = [requestPayload, prompt];
      let lastError: unknown;
      for (const payload of payloadAttempts) {
        try {
          return await Promise.resolve(method.call(session, payload));
        } catch (error) {
          lastError = error;
        }
      }
      throw new Error(
        `Pi session method "${methodName}" failed: ${toErrorMessage(lastError)}`,
      );
    }

    throw new Error(
      'Pi session object does not expose a supported invocation method ' +
      '(expected one of: run, sendMessage, chat, complete, invoke, process)',
    );
  }
}
