import { AsukaLicenseServerErrorResponse, isStandardLicenseServerEnvelope, unwrapStandardLicenseToken } from './license.contract';
import { parseCompactLicenseToken } from './token';
import type {
  AsukaLicenseDeactivateContext,
  AsukaLicenseHttpOptions,
  AsukaLicenseResolveContext,
  AsukaLicenseRefreshContext,
  AsukaSignedLicensePayload,
} from './license.types';

function normalizeEndpoint(url: string | null | undefined): string {
  const value = String(url ?? '').trim();
  if (!value) throw new Error('Missing license endpoint');
  return value;
}

function withTimeout<T>(timeoutMs: number | null | undefined, task: (signal?: AbortSignal) => Promise<T>): Promise<T> {
  if (!timeoutMs || timeoutMs <= 0 || typeof AbortController === 'undefined') {
    return task();
  }

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  return task(controller.signal).finally(() => clearTimeout(timer));
}

class LicenseHttpStatusError extends Error {
  readonly status: number;

  constructor(status: number) {
    super(`License HTTP request failed (${status})`);
    this.name = 'LicenseHttpStatusError';
    this.status = status;
  }
}

const RETRYABLE_LICENSE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);

function isRetryableLicenseRequestError(error: unknown): boolean {
  if (error instanceof AsukaLicenseServerErrorResponse) return error.retryable;
  if (error instanceof LicenseHttpStatusError) return RETRYABLE_LICENSE_HTTP_STATUSES.has(error.status);
  if (error instanceof TypeError) return true;
  const name = typeof error === 'object' && error ? String((error as any).name ?? '') : '';
  return name === 'AbortError' || name === 'TimeoutError';
}

function getRetryCount(value: number | null | undefined): number {
  const retries = Number(value ?? 1);
  if (!Number.isFinite(retries)) return 1;
  return Math.min(3, Math.max(0, Math.floor(retries)));
}

function waitBeforeRetry(delayMs: number, retryIndex: number): Promise<void> {
  const duration = Math.min(2_000, Math.max(0, delayMs) * Math.max(1, retryIndex));
  if (duration === 0) return Promise.resolve();
  return new Promise((resolve) => setTimeout(resolve, duration));
}

function buildDefaultRequestBody(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseResolveContext,
): Record<string, unknown> {
  return {
    key: input.key,
    ...(options.product ? { product: options.product } : null),
    ...(options.audience ? { audience: options.audience } : null),
    ...(options.channel ? { channel: options.channel } : null),
    ...(options.acceptCompactToken ? { acceptCompactToken: true } : null),
    ...(options.includeDomain === false ? null : { domain: input.domain ?? null }),
    ...(options.includeInstanceId === false ? null : { instanceId: input.instanceId ?? null }),
    ...(options.includeVersion === false ? null : { version: input.version ?? null }),
  };
}

function buildDefaultRefreshBody(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseRefreshContext,
): Record<string, unknown> {
  return {
    token: input.token,
    ...(options.product ? { product: options.product } : null),
    ...(options.audience ? { audience: options.audience } : null),
    ...(options.channel ? { channel: options.channel } : null),
    ...(options.acceptCompactToken ? { acceptCompactToken: true } : null),
    ...(options.includeDomain === false ? null : { domain: input.domain ?? null }),
    ...(options.includeInstanceId === false ? null : { instanceId: input.instanceId ?? null }),
    ...(options.includeVersion === false ? null : { version: input.version ?? null }),
  };
}

function buildDefaultDeactivateBody(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseDeactivateContext,
): Record<string, unknown> {
  return {
    ...(input.key ? { key: input.key } : null),
    ...(options.product ? { product: options.product } : null),
    ...(options.channel ? { channel: options.channel } : null),
    ...(options.includeDomain === false ? null : { domain: input.domain ?? null }),
    ...(options.includeInstanceId === false ? null : { instanceId: input.instanceId ?? null }),
    ...(options.includeVersion === false ? null : { version: input.version ?? null }),
  };
}

function isSignedLicensePayload(value: unknown): value is AsukaSignedLicensePayload {
  return Boolean(
    value &&
      typeof value === 'object' &&
      typeof (value as any).signature === 'string' &&
      typeof (value as any).alg === 'string' &&
      typeof (value as any).payload === 'object',
  );
}

function tryParseCompactToken(value: unknown): AsukaSignedLicensePayload | null {
  if (typeof value !== 'string') return null;
  try {
    return parseCompactLicenseToken(value);
  } catch {
    return null;
  }
}

export function parseSignedLicenseResponse(
  raw: unknown,
  options?: Pick<AsukaLicenseHttpOptions, 'parseResponse'>,
): AsukaSignedLicensePayload {
  if (typeof options?.parseResponse === 'function') {
    return parseSignedLicenseResponse(options.parseResponse(raw));
  }

  const standardToken = unwrapStandardLicenseToken(raw);
  if (standardToken) return parseSignedLicenseResponse(standardToken);

  const compactFromRoot = tryParseCompactToken(raw);
  if (compactFromRoot) return compactFromRoot;
  if (isSignedLicensePayload(raw)) return raw;

  if (raw && typeof raw === 'object') {
    const candidates = [
      (raw as any).token,
      (raw as any).compact,
      (raw as any).compactToken,
      (raw as any).signedToken,
      (raw as any).data,
      (raw as any).data?.token,
      (raw as any).data?.compact,
      (raw as any).data?.compactToken,
      (raw as any).data?.signedToken,
    ];

    for (const candidate of candidates) {
      const compactCandidate = tryParseCompactToken(candidate);
      if (compactCandidate) return compactCandidate;
      if (isSignedLicensePayload(candidate)) return candidate;
    }
  }

  throw new Error('Invalid signed license response');
}

async function postJsonOnce(url: string, options: AsukaLicenseHttpOptions, body: unknown): Promise<unknown> {
  return await withTimeout(options.timeoutMs ?? 8000, async (signal) => {
    const response = await fetch(url, {
      method: options.method ?? 'POST',
      credentials: options.credentials,
      headers: {
        'Content-Type': 'application/json',
        ...(options.headers ?? {}),
      },
      body: JSON.stringify(body),
      signal,
    });

    const contentType = String(response.headers.get('content-type') ?? '').toLowerCase();
    const raw = contentType.includes('application/json') ? await response.json() : await response.text();

    if (!response.ok) {
      if (isStandardLicenseServerEnvelope(raw) && raw.ok === false) {
        throw new AsukaLicenseServerErrorResponse(raw.error, raw.meta);
      }
      throw new LicenseHttpStatusError(response.status);
    }

    if (isStandardLicenseServerEnvelope(raw) && raw.ok === false) {
      throw new AsukaLicenseServerErrorResponse(raw.error, raw.meta);
    }

    return raw;
  });
}

async function postJson(url: string, options: AsukaLicenseHttpOptions, body: unknown): Promise<unknown> {
  const retryCount = getRetryCount(options.retryAttempts);
  const retryDelayMs = Number.isFinite(Number(options.retryDelayMs))
    ? Math.max(0, Number(options.retryDelayMs))
    : 150;

  for (let attempt = 0; ; attempt += 1) {
    try {
      return await postJsonOnce(url, options, body);
    } catch (error) {
      if (attempt >= retryCount || !isRetryableLicenseRequestError(error)) throw error;
      await waitBeforeRetry(retryDelayMs, attempt + 1);
    }
  }
}

export async function resolveLicenseByHttp(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseResolveContext,
): Promise<AsukaSignedLicensePayload> {
  const body =
    typeof options.buildRequestBody === 'function'
      ? options.buildRequestBody(input)
      : buildDefaultRequestBody(options, input);

  const raw = await postJson(normalizeEndpoint(options.endpoint), options, body);
  return parseSignedLicenseResponse(raw, options);
}

export async function refreshLicenseByHttp(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseRefreshContext,
): Promise<AsukaSignedLicensePayload> {
  const endpoint = normalizeEndpoint(options.refreshEndpoint || options.endpoint);
  const body =
    typeof options.buildRefreshBody === 'function'
      ? options.buildRefreshBody(input)
      : buildDefaultRefreshBody(options, input);

  const raw = await postJson(endpoint, options, body);
  return parseSignedLicenseResponse(raw, options);
}

export async function deactivateLicenseByHttp(
  options: AsukaLicenseHttpOptions,
  input: AsukaLicenseDeactivateContext,
): Promise<boolean> {
  const endpoint = normalizeEndpoint(options.deactivateEndpoint || options.endpoint);
  const body =
    typeof options.buildDeactivateBody === 'function'
      ? options.buildDeactivateBody(input)
      : buildDefaultDeactivateBody(options, input);

  const raw = await postJson(endpoint, options, body);
  if (typeof raw === 'boolean') return raw;
  if (raw && typeof raw === 'object' && typeof (raw as any).ok === 'boolean') return Boolean((raw as any).ok);
  return true;
}
