import { verifyAsync as verifyEd25519Fallback } from '@noble/ed25519';
import { decodeBase64ToUint8Array, decodeBase64UrlToUint8Array, stableSerialize } from './token';
import type { AsukaLicenseEntitlements, AsukaLicenseVerifier, AsukaSignedLicensePayload } from './license.types';

const LICENSE_VERIFY_DEBUG_PREFIX = '[AsukaDesigner:license:verify]';
const NATIVE_ED25519_TIMEOUT_MS = 500;

function logLicenseVerifyDebug(message: string, details?: unknown): void {
  try {
    if ((globalThis as any).__ASUKA_LICENSE_DEBUG__ !== true) return;
    if (typeof details === 'undefined') console.debug(LICENSE_VERIFY_DEBUG_PREFIX, message);
    else console.debug(LICENSE_VERIFY_DEBUG_PREFIX, message, details);
  } catch {}
}

export interface VerifySignedLicenseInput {
  signed: AsukaSignedLicensePayload;
  publicKey?: string;
  verifier?: AsukaLicenseVerifier;
  expectedAudience?: string;
  domain?: string;
  instanceId?: string;
}

function normalizeHostname(value: string | null | undefined): string {
  const raw = String(value ?? '').trim().toLowerCase();
  if (!raw) return '';
  const withoutProtocol = raw.replace(/^[a-z]+:\/\//i, '');
  const hostOnly = withoutProtocol.split('/')[0].split('?')[0].split('#')[0];
  return hostOnly.split(':')[0].replace(/^\.+|\.+$/g, '');
}

function audienceMatches(actual: string | string[] | null | undefined, expected: string | null | undefined): boolean {
  const target = String(expected ?? '').trim();
  if (!target) return true;
  if (Array.isArray(actual)) return actual.includes(target);
  return String(actual ?? '').trim() === target;
}

export function matchesLicensedDomain(
  actualDomain: string | null | undefined,
  licensedDomain: string | null | undefined,
): boolean {
  const actual = normalizeHostname(actualDomain);
  const expected = normalizeHostname(licensedDomain);
  if (!expected) return true;
  if (!actual) return false;
  return actual === expected || actual.endsWith(`.${expected}`);
}

export function validateLicenseClaims(
  payload: AsukaLicenseEntitlements | null | undefined,
  input: Pick<VerifySignedLicenseInput, 'expectedAudience' | 'domain' | 'instanceId'>,
): boolean {
  if (!payload) return false;
  if (!audienceMatches(payload.aud, input.expectedAudience)) return false;
  if (!matchesLicensedDomain(input.domain, payload.domain)) return false;
  if (payload.instanceId && input.instanceId && payload.instanceId !== input.instanceId) return false;
  return true;
}

async function importEd25519PublicKey(publicKey: string): Promise<CryptoKey> {
  const cleaned = String(publicKey ?? '')
    .replace(/-----BEGIN PUBLIC KEY-----/g, '')
    .replace(/-----END PUBLIC KEY-----/g, '')
    .replace(/\s+/g, '');

  const binary = decodeBase64ToUint8Array(cleaned);
  logLicenseVerifyDebug('Importing Ed25519 public key', { cleanedLength: cleaned.length, binaryLength: binary.length });
  return await crypto.subtle.importKey('spki', binary, { name: 'Ed25519' }, false, ['verify']);
}

const ED25519_SPKI_PREFIX = Uint8Array.from([
  0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
]);

function extractEd25519PublicKeyBytes(publicKey: string): Uint8Array {
  const cleaned = String(publicKey ?? '')
    .replace(/-----BEGIN PUBLIC KEY-----/g, '')
    .replace(/-----END PUBLIC KEY-----/g, '')
    .replace(/\s+/g, '');
  const binary = decodeBase64ToUint8Array(cleaned);
  if (binary.length === 32) return binary;
  if (
    binary.length === ED25519_SPKI_PREFIX.length + 32
    && ED25519_SPKI_PREFIX.every((byte, index) => binary[index] === byte)
  ) {
    return binary.slice(ED25519_SPKI_PREFIX.length);
  }
  throw new Error('Invalid Ed25519 public key encoding.');
}

async function verifyEd25519Signature(
  publicKey: string,
  signature: Uint8Array,
  data: Uint8Array,
): Promise<boolean> {
  try {
    const nativeVerification = (async () => {
      const key = await importEd25519PublicKey(publicKey);
      return await crypto.subtle.verify({ name: 'Ed25519' }, key, signature, data);
    })();
    let timeoutId: ReturnType<typeof setTimeout> | undefined;
    try {
      return await Promise.race([
        nativeVerification,
        new Promise<never>((_resolve, reject) => {
          timeoutId = setTimeout(
            () => reject(new Error('Native Ed25519 verification timed out.')),
            NATIVE_ED25519_TIMEOUT_MS,
          );
        }),
      ]);
    } finally {
      if (timeoutId) clearTimeout(timeoutId);
    }
  } catch (nativeError) {
    logLicenseVerifyDebug('Native Ed25519 unavailable; using strict JS verifier', {
      error: nativeError instanceof Error ? nativeError.message : String(nativeError),
    });
    return await verifyEd25519Fallback(signature, data, extractEd25519PublicKeyBytes(publicKey), {
      zip215: false,
    });
  }
}

async function verifyStructuredPayloadSignature(signed: AsukaSignedLicensePayload, publicKey: string): Promise<boolean> {
  if (!signed || typeof signed !== 'object') return false;
  if (!signed.signature) return false;

  const data = new TextEncoder().encode(stableSerialize(signed.payload));
  const signature = decodeBase64ToUint8Array(signed.signature);
  const verified = await verifyEd25519Signature(publicKey, signature, data);
  logLicenseVerifyDebug('Structured payload signature verification', { verified, dataLength: data.length, signatureLength: signature.length });
  return verified;
}

async function verifyCompactTokenSignature(signed: AsukaSignedLicensePayload, publicKey: string): Promise<boolean> {
  if (!signed.compact || !signed.signingInput) return false;
  const data = new TextEncoder().encode(signed.signingInput);
  const signature = decodeBase64UrlToUint8Array(signed.signature);
  const verified = await verifyEd25519Signature(publicKey, signature, data);
  logLicenseVerifyDebug('Compact token signature verification', { verified, signingInputLength: data.length, signatureLength: signature.length, signingInputPreview: signed.signingInput?.slice(0, 48) });
  return verified;
}

export async function verifySignedLicensePayload(input: VerifySignedLicenseInput): Promise<boolean> {
  const { signed, publicKey, verifier } = input;
  if (!signed || typeof signed !== 'object') return false;

  let signatureValid = false;

  if (typeof verifier === 'function') {
    try {
      signatureValid = !!(await verifier(signed));
      logLicenseVerifyDebug('Custom verifier result', { signatureValid });
    } catch (error) {
      logLicenseVerifyDebug('Custom verifier threw', { error: error instanceof Error ? error.message : String(error) });
      signatureValid = false;
    }
  } else {
    const subtle = globalThis.crypto?.subtle;
    if (!publicKey) {
      logLicenseVerifyDebug('Verification aborted before crypto', { hasSubtle: !!subtle, hasPublicKey: !!publicKey });
      return false;
    }

    try {
      if (signed.format === 'compact' || !!signed.compact) {
        if (signed.alg !== 'EdDSA' && signed.alg !== 'Ed25519') {
          logLicenseVerifyDebug('Unsupported compact token alg', { alg: signed.alg });
          return false;
        }
        signatureValid = await verifyCompactTokenSignature(signed, publicKey);
      } else {
        if (signed.alg != 'Ed25519' && signed.alg != 'EdDSA') {
          logLicenseVerifyDebug('Unsupported structured payload alg', { alg: signed.alg });
          return false;
        }
        signatureValid = await verifyStructuredPayloadSignature(signed, publicKey);
      }
    } catch (error) {
      logLicenseVerifyDebug('Verification threw', { error: error instanceof Error ? error.message : String(error) });
      signatureValid = false;
    }
  }

  if (!signatureValid) {
    logLicenseVerifyDebug('Signature invalid', {
      alg: signed.alg,
      kid: signed.kid,
      format: signed.format,
      hasCompact: !!signed.compact,
      hasSigningInput: !!signed.signingInput,
      payloadAud: (signed.payload as any)?.aud,
      payloadDomain: (signed.payload as any)?.domain,
      payloadInstanceId: (signed.payload as any)?.instanceId,
      expectedAudience: input.expectedAudience,
      expectedDomain: input.domain,
      expectedInstanceId: input.instanceId,
    });
    return false;
  }

  const claimsValid = validateLicenseClaims(signed.payload, input);
  logLicenseVerifyDebug('Claim validation result', {
    claimsValid,
    payloadAud: (signed.payload as any)?.aud,
    payloadDomain: (signed.payload as any)?.domain,
    payloadInstanceId: (signed.payload as any)?.instanceId,
    expectedAudience: input.expectedAudience,
    expectedDomain: input.domain,
    expectedInstanceId: input.instanceId,
  });

  return claimsValid;
}
