import { HttpStatus } from '@nestjs/common';
import { DomainException, ErrorCode, LoggerService } from '@modules/shared';

/**
 * Shared transport + JSON-parsing helpers for Anthropic-compatible AI endpoints.
 * Used by both `get-listing-ai-advice` and `get-project-ai-advice`.
 *
 * Sends BOTH `x-api-key` (native Anthropic) and `Authorization: Bearer` (proxy
 * gateways like chat.trollllm.xyz). Anthropic ignores unknown headers so the
 * duplicate is harmless.
 */

const ANTHROPIC_TIMEOUT_MS = 30_000;

export interface AnthropicUsage {
  input: number;
  cacheCreation: number;
  cacheRead: number;
  output: number;
}

interface AnthropicMessagePart {
  type: string;
  text?: string;
}

interface AnthropicRawResponse {
  content?: AnthropicMessagePart[];
  usage?: {
    input_tokens?: number;
    output_tokens?: number;
    cache_creation_input_tokens?: number;
    cache_read_input_tokens?: number;
  };
}

export interface CallAnthropicArgs {
  apiUrl: string;
  apiKey: string;
  model: string;
  systemPrompt: string;
  userPrompt: string;
  /** Optional max_tokens, default 1024. */
  maxTokens?: number;
  /** Logger for non-2xx responses and timeouts. */
  logger: LoggerService;
  /** Used in log tags, e.g. "ai-advice" or "project-ai-advice". */
  tag?: string;
}

export interface CallAnthropicResult {
  /** The decoded first text content block, with any ```json fence stripped. */
  text: string;
  usage: AnthropicUsage;
}

export async function callAnthropicJson(
  args: CallAnthropicArgs,
): Promise<CallAnthropicResult> {
  const tag = args.tag ?? 'ai-json';
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ANTHROPIC_TIMEOUT_MS);

  try {
    const res = await fetch(`${args.apiUrl.replace(/\/$/, '')}/messages`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // Dual auth: native Anthropic uses `x-api-key`; OpenAI-style proxies
        // use `Authorization: Bearer`. Sending both is safe.
        'x-api-key': args.apiKey,
        Authorization: `Bearer ${args.apiKey}`,
        'anthropic-version': '2023-06-01',
        'anthropic-beta': 'prompt-caching-2024-07-31',
      },
      signal: controller.signal,
      body: JSON.stringify({
        model: args.model,
        max_tokens: args.maxTokens ?? 1024,
        system: [
          {
            type: 'text',
            text: args.systemPrompt,
            cache_control: { type: 'ephemeral' },
          },
        ],
        messages: [{ role: 'user', content: args.userPrompt }],
      }),
    });

    if (!res.ok) {
      const body = await res.text().catch(() => '');
      args.logger.error(
        `[${tag}] Anthropic non-2xx ${res.status}: ${body.slice(0, 500)}`,
      );
      throw new DomainException(
        ErrorCode.AI_PROVIDER_ERROR,
        'Không gọi được dịch vụ AI. Vui lòng thử lại sau.',
        HttpStatus.BAD_GATEWAY,
      );
    }

    const raw = (await res.json()) as AnthropicRawResponse;
    const block = raw.content?.find((c) => c.type === 'text');
    const text = block?.text?.trim();
    if (!text) {
      throw new DomainException(
        ErrorCode.AI_PROVIDER_ERROR,
        'AI trả về nội dung trống.',
        HttpStatus.BAD_GATEWAY,
      );
    }

    return {
      text: stripJsonFence(text),
      usage: {
        input: raw.usage?.input_tokens ?? 0,
        cacheCreation: raw.usage?.cache_creation_input_tokens ?? 0,
        cacheRead: raw.usage?.cache_read_input_tokens ?? 0,
        output: raw.usage?.output_tokens ?? 0,
      },
    };
  } catch (err) {
    if (err instanceof DomainException) throw err;
    const isAbort =
      err instanceof Error && (err.name === 'AbortError' || /aborted/i.test(err.message));
    args.logger.error(
      `[${tag}] fetch failed (${isAbort ? 'timeout' : 'network'}): ${err instanceof Error ? err.message : err}`,
    );
    throw new DomainException(
      ErrorCode.AI_PROVIDER_ERROR,
      isAbort
        ? 'AI không phản hồi kịp. Vui lòng thử lại.'
        : 'Không gọi được dịch vụ AI. Vui lòng thử lại sau.',
      HttpStatus.BAD_GATEWAY,
    );
  } finally {
    clearTimeout(timer);
  }
}

export function parseJsonObject(text: string): Record<string, unknown> {
  let obj: unknown;
  try {
    obj = JSON.parse(text);
  } catch {
    throw new DomainException(
      ErrorCode.AI_PROVIDER_ERROR,
      'AI trả về JSON không hợp lệ.',
      HttpStatus.BAD_GATEWAY,
    );
  }
  if (!isRecord(obj)) {
    throw new DomainException(
      ErrorCode.AI_PROVIDER_ERROR,
      'AI trả về sai cấu trúc JSON.',
      HttpStatus.BAD_GATEWAY,
    );
  }
  return obj;
}

export function jsonShapeError(field?: string): DomainException {
  return new DomainException(
    ErrorCode.AI_PROVIDER_ERROR,
    field
      ? `AI trả về sai cấu trúc trường '${field}'.`
      : 'AI trả về sai cấu trúc JSON.',
    HttpStatus.BAD_GATEWAY,
  );
}

// --------------------------------------------------------------------------
// Primitive coercers — shared between listing + project handlers.
// --------------------------------------------------------------------------

export function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v != null && !Array.isArray(v);
}

export function asInt(v: unknown): number | null {
  if (typeof v === 'number' && Number.isFinite(v)) return Math.round(v);
  if (typeof v === 'string') {
    const n = Number(v.replace(/[^\d.-]/g, ''));
    if (Number.isFinite(n)) return Math.round(n);
  }
  return null;
}

export function asString(v: unknown): string | null {
  if (typeof v === 'string' && v.trim().length > 0) return v.trim();
  return null;
}

export function asStringArray(v: unknown): string[] | null {
  if (!Array.isArray(v)) return null;
  const out: string[] = [];
  for (const item of v) {
    if (typeof item === 'string' && item.trim().length > 0) out.push(item.trim());
  }
  return out;
}

function stripJsonFence(text: string): string {
  const fenceMatch = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
  if (fenceMatch && fenceMatch[1]) return fenceMatch[1].trim();
  return text;
}
