import { appendFile } from "node:fs/promises";

import { ProductierApiError } from "./client.mjs";

const RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
const NETWORK_ERROR_PATTERN = /(network|fetch|socket|timeout|timed out|econnreset|enotfound|eai_again)/i;

function resolveNumberOption(value, envValue, fallback, { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = {}) {
  const candidate = value ?? envValue;
  if (candidate === undefined || candidate === null || candidate === "") {
    return fallback;
  }

  const parsed = Number.parseInt(String(candidate), 10);
  if (Number.isNaN(parsed)) {
    return fallback;
  }

  if (parsed < min || parsed > max) {
    return fallback;
  }

  return parsed;
}

function calculateRetryDelayMs(attempt, config, randomFn) {
  const exponent = Math.max(0, attempt - 1);
  const baseDelay = Math.min(config.maxDelayMs, config.baseDelayMs * (2 ** exponent));
  const jitter = config.jitterMs > 0 ? Math.floor(randomFn() * (config.jitterMs + 1)) : 0;
  return baseDelay + jitter;
}

export class ToolRateLimitError extends Error {
  constructor(toolName, retryAfterMs) {
    const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
    super(`rate limit exceeded for ${toolName}; retry in ${retryAfterSeconds}s`);
    this.name = "ToolRateLimitError";
    this.status = 429;
    this.code = "tool_rate_limited";
    this.retryAfterMs = retryAfterMs;
  }
}

export class ToolInvocationError extends Error {
  constructor(cause, attempts, retryable) {
    super(cause instanceof Error ? cause.message : "tool invocation failed");
    this.name = "ToolInvocationError";
    this.cause = cause;
    this.attempts = attempts;
    this.retryable = retryable;
  }
}

export function isRetryableError(error) {
  if (error instanceof ProductierApiError) {
    return RETRYABLE_STATUSES.has(Number(error.status));
  }

  if (error instanceof ToolRateLimitError) {
    return false;
  }

  if (!(error instanceof Error)) {
    return false;
  }

  if (error.name === "AbortError" || error.name === "TimeoutError") {
    return true;
  }

  if (error instanceof TypeError) {
    return true;
  }

  return NETWORK_ERROR_PATTERN.test(error.message || "");
}

export function createRuntimeConfig(options = {}) {
  return {
    retry: {
      maxAttempts: resolveNumberOption(
        options.retryMaxAttempts ?? options.retry?.maxAttempts,
        process.env.PRODUCTIER_TOOL_RETRY_MAX_ATTEMPTS,
        3,
        { min: 1, max: 10 },
      ),
      baseDelayMs: resolveNumberOption(
        options.retryBaseDelayMs ?? options.retry?.baseDelayMs,
        process.env.PRODUCTIER_TOOL_RETRY_BASE_DELAY_MS,
        150,
        { min: 0, max: 60000 },
      ),
      maxDelayMs: resolveNumberOption(
        options.retryMaxDelayMs ?? options.retry?.maxDelayMs,
        process.env.PRODUCTIER_TOOL_RETRY_MAX_DELAY_MS,
        2000,
        { min: 0, max: 60000 },
      ),
      jitterMs: resolveNumberOption(
        options.retryJitterMs ?? options.retry?.jitterMs,
        process.env.PRODUCTIER_TOOL_RETRY_JITTER_MS,
        50,
        { min: 0, max: 10000 },
      )
    },
    rateLimit: {
      maxCalls: resolveNumberOption(
        options.rateLimitMaxCalls ?? options.rateLimit?.maxCalls,
        process.env.PRODUCTIER_TOOL_RATE_LIMIT_MAX_CALLS,
        120,
        { min: 0, max: 100000 },
      ),
      windowMs: resolveNumberOption(
        options.rateLimitWindowMs ?? options.rateLimit?.windowMs,
        process.env.PRODUCTIER_TOOL_RATE_LIMIT_WINDOW_MS,
        60000,
        { min: 1, max: 3600000 },
      )
    },
    auditLogPath: options.auditLogPath ?? process.env.PRODUCTIER_AUDIT_LOG_PATH ?? ""
  };
}

export function createRateLimiter(config = {}, options = {}) {
  const maxCalls = Number(config.maxCalls ?? 0);
  const windowMs = Number(config.windowMs ?? 60000);
  const nowFn = typeof options.nowFn === "function" ? options.nowFn : Date.now;

  if (maxCalls < 1 || windowMs < 1) {
    return {
      consume() {
        return;
      }
    };
  }

  const buckets = new Map();

  return {
    consume(toolName) {
      const now = nowFn();
      const windowStart = now - windowMs;
      const existing = buckets.get(toolName) ?? [];
      const withinWindow = existing.filter(timestamp => timestamp > windowStart);

      if (withinWindow.length >= maxCalls) {
        const oldestTimestamp = withinWindow[0];
        const retryAfterMs = Math.max(1, windowMs - (now - oldestTimestamp));
        throw new ToolRateLimitError(toolName, retryAfterMs);
      }

      withinWindow.push(now);
      buckets.set(toolName, withinWindow);
    }
  };
}

export function createAuditLogger(options = {}, runtimeConfig = {}) {
  const callback = options.auditLogFn;
  if (typeof callback === "function") {
    return {
      async log(entry) {
        try {
          await callback(entry);
        } catch {
          // Never fail tool execution because audit logging failed.
        }
      }
    };
  }

  const auditLogPath = runtimeConfig.auditLogPath;
  if (!auditLogPath) {
    return {
      async log() {
        return;
      }
    };
  }

  return {
    async log(entry) {
      try {
        await appendFile(auditLogPath, `${JSON.stringify(entry)}\n`, "utf8");
      } catch {
        // Never fail tool execution because audit logging failed.
      }
    }
  };
}

export async function invokeWithRetry(operation, config = {}, options = {}) {
  const maxAttempts = Math.max(1, Number(config.maxAttempts ?? 1));
  const sleepFn = typeof options.sleepFn === "function" ? options.sleepFn : ms => new Promise(resolve => setTimeout(resolve, ms));
  const randomFn = typeof options.randomFn === "function" ? options.randomFn : Math.random;

  let attempts = 0;
  while (attempts < maxAttempts) {
    attempts += 1;
    try {
      const data = await operation();
      return { data, attempts };
    } catch (cause) {
      const retryable = isRetryableError(cause);
      const shouldRetry = retryable && attempts < maxAttempts;

      if (!shouldRetry) {
        throw new ToolInvocationError(cause, attempts, retryable);
      }

      const delayMs = calculateRetryDelayMs(attempts, config, randomFn);
      if (delayMs > 0) {
        await sleepFn(delayMs);
      }
    }
  }

  throw new ToolInvocationError(new Error("tool invocation exhausted retry loop"), maxAttempts, false);
}

export function unwrapInvocationError(error) {
  if (error instanceof ToolInvocationError) {
    return {
      cause: error.cause,
      attempts: error.attempts,
      retryable: error.retryable
    };
  }

  return {
    cause: error,
    attempts: 1,
    retryable: isRetryableError(error)
  };
}
