import type { Plugin, Hooks } from '@opencode-ai/plugin';
import type { Message, Part, TextPart } from '@opencode-ai/sdk';
import type { EventSessionIdle } from '@opencode-ai/sdk';

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

export const SIDE_EFFECT_TOOLS = new Set([
  'Write', 'Edit', 'MultiEdit', 'Bash', 'NotebookEdit',
]);

export const MAX_CHUNK = 10_000;
export const MAX_CONTEXT_CHARS = 9_500;
export const MIN_PROMPT_LEN = 4;
export const POST_TIMEOUT = 8_000;
export const COMPOSE_TIMEOUT = 13_000;
export const AFTER_TURN_TIMEOUT = 10_000;

export function getApiBaseUrl(): string {
  const env = (process.env.OG_MEMORY_URL || '').trim();
  if (env) return env.replace(/\/+$/, '');
  return 'http://localhost:8090';
}

export function getIdentity(): { accountId: string; userId: string; agentId: string } {
  return {
    accountId: process.env.OG_MEMORY_ACCOUNT_ID || process.env.OG_ACCOUNT_ID || 'acct-demo',
    userId:    process.env.OG_MEMORY_USER_ID    || process.env.OG_USER_ID    || 'u-opencode',
    agentId:   process.env.OG_MEMORY_AGENT_ID   || process.env.OG_AGENT_ID   || 'opencode',
  };
}

export function getApiKey(): string | null {
  const v = (process.env.OG_AUTH_API_KEY || '').trim();
  return v || null;
}

export function buildHeaders(): Record<string, string> {
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  const key = getApiKey();
  if (key) {
    const { accountId, userId } = getIdentity();
    headers['X-API-Key']     = key;
    headers['X-Account-ID']  = accountId;
    headers['X-User-ID']     = userId;
  }
  return headers;
}

export function baseCtx(sessionId: string): Record<string, string> {
  const { accountId, userId, agentId } = getIdentity();
  return { accountId, userId, agentId, sessionId };
}

// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------

let _opencodeClient: ReturnType<typeof import('@opencode-ai/sdk').createOpencodeClient> | null = null;

export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

export function setOpencodeClient(client: typeof _opencodeClient): void {
  _opencodeClient = client;
}

export function getOpencodeClient(): typeof _opencodeClient {
  return _opencodeClient;
}

const LOG_LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };

export function getMinLogLevel(): LogLevel {
  return (process.env.OG_LOG_LEVEL as LogLevel) || 'info';
}

export async function log(level: LogLevel, message: string, extra?: Record<string, unknown>): Promise<void> {
  if (LOG_LEVELS[level] < LOG_LEVELS[getMinLogLevel()]) return;
  const effectiveLevel: LogLevel = level === 'debug' ? 'info' : level;
  if (_opencodeClient) {
    try {
      const body: { service: string; level: LogLevel; message: string; extra?: { [key: string]: unknown } } = {
        service: 'oG-Memory', level: effectiveLevel, message,
      };
      if (extra) body.extra = extra;
      await _opencodeClient.app.log({ body });
    } catch {
      console.error(`[oG-Memory] ${level}: ${message}`, extra ?? '');
    }
  } else {
    console.error(`[oG-Memory] ${level}: ${message}`, extra ?? '');
  }
}

// ---------------------------------------------------------------------------
// HTTP helper
// ---------------------------------------------------------------------------

export async function postJson(url: string, body: unknown, timeoutMs: number): Promise<unknown | null> {
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const res = await fetch(url, {
      method: 'POST',
      headers: buildHeaders(),
      body: JSON.stringify(body),
      signal: controller.signal,
    });
    clearTimeout(timer);
    if (!res.ok) {
      await log('error', `HTTP ${res.status} from ${url}`);
      return null;
    }
    const data = await res.json();
    await log('debug', `Response ${res.status}: ${JSON.stringify(data).slice(0, 200)}`);
    return data;
  } catch (e) {
    await log('error', `postJson error: ${e}`);
    return null;
  }
}

// ---------------------------------------------------------------------------
// 1. PostToolUse → tool.execute.after
// ---------------------------------------------------------------------------

export function truncate(text: string, maxLen: number = MAX_CHUNK): string {
  if (text.length <= maxLen) return text;
  return text.slice(0, maxLen) + `\n… [${text.length - maxLen} more chars omitted]`;
}

export function jsonStringify(obj: unknown): string {
  return JSON.stringify(obj, (_, value) => {
    if (typeof value === 'bigint' || value instanceof RegExp || value instanceof Date) return String(value);
    return value;
  });
}

export function jsonChunk(obj: unknown): string {
  const raw = jsonStringify(obj);
  return truncate(raw);
}

export function buildToolText(toolName: string, toolInput: unknown, toolOutput: unknown): string {
  return [
    `[PostToolUse] ${toolName}`,
    `tool_input: ${jsonChunk(toolInput)}`,
    `tool_response: ${jsonChunk(toolOutput)}`,
  ].join('\n');
}

export async function postSessionMessage(sessionId: string, role: string, content: string): Promise<boolean> {
  const url = `${getApiBaseUrl()}/api/v1/sessions/${sessionId}/messages`;
  const body = { ...baseCtx(sessionId), role, content };
  await log('debug', `POST ${url} role=${role} content_len=${content.length}`);
  const res = await postJson(url, body, POST_TIMEOUT);
  if (res === null) {
    await log('error', `Failed to post session message: session=${sessionId} role=${role}`);
    return false;
  }
  return true;
}

const toolExecuteAfter: Hooks['tool.execute.after'] = async (input, output) => {
  const sessionId = input.sessionID;
  if (!sessionId) {
    await log('debug', 'No sessionID, skip');
    return;
  }

  const toolName = input.tool;
  if (!SIDE_EFFECT_TOOLS.has(toolName)) {
    await log('debug', `skip non-side-effect tool: ${toolName}`);
    return;
  }

  const text = buildToolText(toolName, input.args, output.output);
  if (!text.trim()) return;

  try {
    await postSessionMessage(sessionId, 'tool', text);
  } catch (e) {
    await log('error', `toolExecuteAfter error (logged only): ${e}`);
  }
};

// ---------------------------------------------------------------------------
// 2. UserPromptSubmit → chat.message
// ---------------------------------------------------------------------------

export function buildAdditionalContext(data: { identityContext?: string; retrievedEvidence?: string }): string | null {
  const parts: string[] = [];

  const identity = (data.identityContext || '').trim();
  if (identity) parts.push(identity);

  const evidence = (data.retrievedEvidence || '').trim();
  if (evidence) parts.push(evidence);

  if (parts.length === 0) return null;

  let body = parts.join('\n\n');
  if (body.length > MAX_CONTEXT_CHARS) {
    body = body.slice(0, MAX_CONTEXT_CHARS);
  }

  return `[oG-Memory]\n${body}`;
}

export function extractPromptText(output: { message: { role: string }; parts: Array<{ type: string; text?: string }> }): string {
  const textParts = output.parts.filter((p) => p.type === 'text' && p.text);
  return textParts.map((p) => p.text!).join('\n').trim();
}

const chatMessage: Hooks['chat.message'] = async (input, output) => {
  const sessionId = input.sessionID;
  if (!sessionId) {
    await log('debug', 'chat.message: no sessionID, skip');
    return;
  }

  const prompt = extractPromptText(output);

  if (prompt.length < MIN_PROMPT_LEN) {
    await log('debug', `chat.message: prompt too short (${prompt.length} chars), skipping`);
    return;
  }
  if (prompt.startsWith('/')) {
    await log('debug', 'chat.message: slash command, skipping');
    return;
  }

  const url = `${getApiBaseUrl()}/api/v1/compose`;
  const body = { ...baseCtx(sessionId), prompt };
  const data = await postJson(url, body, COMPOSE_TIMEOUT) as { identityContext?: string; retrievedEvidence?: string } | null;
  if (data === null) return;

  const additional = buildAdditionalContext(data);
  if (additional === null) {
    await log('info', 'chat.message: no relevant context returned');
    return;
  }

  output.parts.push({
    type: 'text',
    text: additional,
    synthetic: true,
  } as any);
};

// ---------------------------------------------------------------------------
// 3. Stop → session.idle
// ---------------------------------------------------------------------------

export interface SimpleMessage {
  role: 'user' | 'assistant';
  content: string;
}

export const sentCountMap = new Map<string, number>();

export function getSentCount(sessionId: string): number {
  return sentCountMap.get(sessionId) ?? 0;
}

export function setSentCount(sessionId: string, count: number): void {
  sentCountMap.set(sessionId, count);
}

export function messagesToSimple(
  msgs: Array<{ info: Message; parts: Array<Part> }>,
): SimpleMessage[] {
  const result: SimpleMessage[] = [];
  for (const msg of msgs) {
    const role = msg.info.role;
    if (role !== 'user' && role !== 'assistant') continue;

    if ('error' in msg.info && msg.info.error) continue;

    const textParts = msg.parts
      .filter((p): p is TextPart => p.type === 'text')
      .map((p) => p.text);
    const content = textParts.join('\n').trim();
    if (content) result.push({ role, content });
  }
  return result;
}

export async function afterTurn(sessionId: string, hookEvent: string): Promise<void> {
  const client = getOpencodeClient();
  if (!client) {
    await log('error', 'afterTurn: no opencode client, skip');
    return;
  }

  try {
    const res = await client.session.messages({
      path: { id: sessionId },
    });
    if (!res.data) {
      await log('error', 'afterTurn: failed to fetch messages');
      return;
    }

    const allMessages = messagesToSimple(res.data as Array<{ info: Message; parts: Array<Part> }>);
    const lastSent = getSentCount(sessionId);
    const totalMsgs = allMessages.length;

    if (totalMsgs <= lastSent) {
      await log('debug', `afterTurn: no new content (sentCount=${lastSent}, total=${totalMsgs})`);
      return;
    }

    const newMessages = allMessages.slice(lastSent);

    if (newMessages.length === 0) {
      await log('debug', 'afterTurn: no valid messages after filtering');
      setSentCount(sessionId, totalMsgs);
      return;
    }

    const url = `${getApiBaseUrl()}/api/v1/after_turn`;
    const body = { ...baseCtx(sessionId), messages: newMessages, hook_event_name: hookEvent };
    await log('debug', `POST ${url} session=${sessionId} msgs=${newMessages.length} event=${hookEvent}`);
    const result = await postJson(url, body, AFTER_TURN_TIMEOUT);

    if (result !== null) {
      setSentCount(sessionId, totalMsgs);
      await log('info', `afterTurn: sentCount updated to ${totalMsgs}`);
    } else {
      await log('error', 'afterTurn: ingest failed, sentCount NOT updated (will retry next hook)');
    }
  } catch (e) {
    await log('error', `afterTurn error: ${e}`);
  }
}

const onEvent: Hooks['event'] = async (input) => {
  if (input.event.type === 'session.idle') {
    const sessionId = (input.event as EventSessionIdle).properties.sessionID;
    afterTurn(sessionId, 'Stop').catch((e) =>
      log('error', `onEvent (session.idle) fire-and-forget error: ${e}`),
    );
  }
};

// ---------------------------------------------------------------------------
// 4. PreCompact → experimental.session.compacting
// ---------------------------------------------------------------------------

const sessionCompacting: Hooks['experimental.session.compacting'] = async (input) => {
  const sessionId = input.sessionID;
  if (!sessionId) {
    await log('debug', 'sessionCompacting: no sessionID, skip');
    return;
  }

  await afterTurn(sessionId, 'PreCompact');
};

// ---------------------------------------------------------------------------
// Plugin entry
// ---------------------------------------------------------------------------

const oGMemoryPlugin: Plugin = async (ctx) => {
  setOpencodeClient(ctx.client);

  return {
    'tool.execute.after': toolExecuteAfter,
    'chat.message': chatMessage,
    event: onEvent,
    'experimental.session.compacting': sessionCompacting,
  };
};

export default { id: 'oG-Memory', server: oGMemoryPlugin };