import type { CompleteOptions, LlmConfig, LlmErrorKind, LlmResult } from './types';

const DEFAULT_TIMEOUT = 30_000;
const DEFAULT_MAX_TOKENS = 1024;

function err<T>(error: LlmErrorKind, message: string): LlmResult<T> {
  return { ok: false, error, message };
}

function trimSlash(url: string): string {
  return url.replace(/\/+$/, '');
}

function jsonInstruction(): string {
  return '\n\nRespond with ONLY a single valid JSON object — no prose, no markdown code fences — matching the requested shape exactly.';
}

/** Best-effort extraction of a JSON object from a model response. */
function tryParseJson(text: string): unknown {
  const trimmed = text.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
  const start = trimmed.indexOf('{');
  const end = trimmed.lastIndexOf('}');
  const candidate = start >= 0 && end > start ? trimmed.slice(start, end + 1) : trimmed;
  try {
    return JSON.parse(candidate);
  } catch {
    return undefined;
  }
}

async function safeText(res: Response): Promise<string> {
  try {
    return (await res.text()).slice(0, 300);
  } catch {
    return '';
  }
}

interface Request {
  url: string;
  headers: Record<string, string>;
  body: unknown;
}

function buildAnthropic(cfg: LlmConfig, opts: CompleteOptions, wantJson: boolean): Request {
  return {
    url: `${trimSlash(cfg.baseUrl)}/v1/messages`,
    headers: {
      'content-type': 'application/json',
      'x-api-key': cfg.apiKey,
      'anthropic-version': '2023-06-01',
      'anthropic-dangerous-direct-browser-access': 'true',
    },
    body: {
      model: cfg.model,
      max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
      system: wantJson ? opts.system + jsonInstruction() : opts.system,
      messages: [{ role: 'user', content: opts.user }],
    },
  };
}

function buildOpenai(cfg: LlmConfig, opts: CompleteOptions, wantJson: boolean): Request {
  return {
    url: `${trimSlash(cfg.baseUrl)}/chat/completions`,
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${cfg.apiKey}`,
    },
    body: {
      model: cfg.model,
      max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
      messages: [
        { role: 'system', content: wantJson ? opts.system + jsonInstruction() : opts.system },
        { role: 'user', content: opts.user },
      ],
      ...(wantJson ? { response_format: { type: 'json_object' } } : {}),
    },
  };
}

function extractText(provider: LlmConfig['provider'], json: unknown): string {
  const j = json as Record<string, unknown>;
  if (provider === 'anthropic') {
    const content = j.content as Array<{ type?: string; text?: string }> | undefined;
    return content?.find((b) => b.type === 'text')?.text ?? '';
  }
  const choices = j.choices as Array<{ message?: { content?: string } }> | undefined;
  return choices?.[0]?.message?.content ?? '';
}

/**
 * Provider-agnostic completion. Supports Anthropic Messages and OpenAI-compatible
 * Chat Completions via fetch (no SDK). With `opts.schema`, requests JSON and
 * validates it — returning `{ok:true,data}`; otherwise returns `{ok:true,text}`.
 * Never throws and never includes the API key in an error message.
 */
export async function complete<T>(cfg: LlmConfig, opts: CompleteOptions<T>): Promise<LlmResult<T>> {
  if (!cfg.enabled) return err('disabled', 'The assistant is turned off in Settings.');
  if (!cfg.apiKey.trim()) return err('no-key', 'No API key configured in Settings.');
  if (!cfg.baseUrl.trim()) return err('network', 'No provider base URL configured.');

  const wantJson = !!opts.schema;
  const controller = new AbortController();
  const onParentAbort = () => controller.abort();
  if (opts.signal) opts.signal.addEventListener('abort', onParentAbort);
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT);

  try {
    const req = cfg.provider === 'anthropic' ? buildAnthropic(cfg, opts, wantJson) : buildOpenai(cfg, opts, wantJson);

    let res: Response;
    try {
      res = await fetch(req.url, {
        method: 'POST',
        headers: req.headers,
        body: JSON.stringify(req.body),
        signal: controller.signal,
      });
    } catch (e) {
      if (controller.signal.aborted) return err('timeout', 'The request timed out.');
      const msg = e instanceof Error ? e.message : String(e);
      if (/content security policy|csp/i.test(msg)) {
        return err('csp', 'Blocked by content security policy. Check the provider base URL.');
      }
      return err('network', `Could not reach the provider. Check the base URL and your connection.`);
    }

    if (!res.ok) {
      return err('http', `Provider returned HTTP ${res.status}. ${await safeText(res)}`.trim());
    }

    let json: unknown;
    try {
      json = await res.json();
    } catch {
      return err('parse', 'Provider returned a non-JSON response.');
    }

    const text = extractText(cfg.provider, json);
    if (!wantJson) return { ok: true, text };

    const parsed = tryParseJson(text);
    if (parsed === undefined) return err('parse', 'The model did not return valid JSON.');
    const result = opts.schema!.safeParse(parsed);
    if (!result.success) return err('parse', 'The model output did not match the expected shape.');
    return { ok: true, data: result.data };
  } finally {
    clearTimeout(timer);
    if (opts.signal) opts.signal.removeEventListener('abort', onParentAbort);
  }
}

/** Lightweight reachability/auth check used by the Settings "Test connection" button. */
export async function testConnection(cfg: LlmConfig): Promise<LlmResult<string>> {
  return complete({ ...cfg, enabled: true }, {
    system: 'You are a connectivity check. Reply with the single word: ok.',
    user: 'ping',
    maxTokens: 16,
    timeoutMs: 15_000,
  });
}
