const encoder = new TextEncoder();
const WEB_CRYPTO_REQUIRED_MESSAGE = 'Регистрация и подпись блоков требуют WebCrypto (crypto.subtle). Откройте приложение через HTTPS или localhost в современном браузере и повторите попытку.';
import { argon2idAsync } from 'https://esm.sh/@noble/hashes@1.8.0/argon2.js';

function getCryptoApi() {
  const api = globalThis.crypto;
  if (!api || typeof api.getRandomValues !== 'function') {
    throw new Error(WEB_CRYPTO_REQUIRED_MESSAGE);
  }
  return api;
}

function getSubtleApi() {
  const api = getCryptoApi();
  if (!api.subtle) {
    throw new Error(WEB_CRYPTO_REQUIRED_MESSAGE);
  }
  return api.subtle;
}


function base64UrlToBase64(value) {
  const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
  const padLen = (4 - (normalized.length % 4)) % 4;
  return normalized + '='.repeat(padLen);
}

const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

export function bytesToBase58(bytes) {
  const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
  if (input.length === 0) return '';

  const digits = [];
  for (let i = 0; i < input.length; i += 1) {
    let carry = input[i];
    for (let j = 0; j < digits.length; j += 1) {
      const value = (digits[j] * 256) + carry;
      digits[j] = value % 58;
      carry = Math.floor(value / 58);
    }
    while (carry > 0) {
      digits.push(carry % 58);
      carry = Math.floor(carry / 58);
    }
  }

  for (let i = 0; i < input.length && input[i] === 0; i += 1) {
    digits.push(0);
  }

  return digits.reverse().map((digit) => BASE58_ALPHABET[digit]).join('');
}

export function base58ToBytes(value) {
  const text = String(value || '').trim();
  if (!text) return new Uint8Array();

  const digits = [];
  for (let i = 0; i < text.length; i += 1) {
    const char = text[i];
    const index = BASE58_ALPHABET.indexOf(char);
    if (index < 0) throw new Error(`Недопустимый символ base58: ${char}`);
    let carry = index;
    for (let j = 0; j < digits.length; j += 1) {
      const acc = (digits[j] * 58) + carry;
      digits[j] = acc & 0xff;
      carry = acc >> 8;
    }
    while (carry > 0) {
      digits.push(carry & 0xff);
      carry >>= 8;
    }
  }

  for (let i = 0; i < text.length && text[i] === '1'; i += 1) {
    digits.push(0);
  }

  return new Uint8Array(digits.reverse());
}

export function randomBase64(byteLen = 32) {
  const bytes = getCryptoApi().getRandomValues(new Uint8Array(byteLen));
  return bytesToBase64(bytes);
}

export function bytesToBase64(bytes) {
  let binary = '';
  bytes.forEach((b) => {
    binary += String.fromCharCode(b);
  });
  return btoa(binary);
}

export function base64ToBytes(base64) {
  const normalized = (base64 || '').trim();
  const binary = atob(normalized);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i += 1) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}

export function utf8Bytes(value) {
  return encoder.encode(value);
}

export async function sha256Bytes(bytes) {
  const digest = await getSubtleApi().digest('SHA-256', bytes);
  return new Uint8Array(digest);
}

export async function sha256Text(text) {
  return sha256Bytes(utf8Bytes(text));
}

function normalizeLoginForKdf(login) {
  return String(login || '').trim().toLowerCase();
}

async function makeArgon2Salt(login, suffix) {
  const normalizedLogin = normalizeLoginForKdf(login);
  const normalizedSuffix = String(suffix || '').trim();
  const saltSource = `shine-auth-v2|login=${normalizedLogin}|suffix=${normalizedSuffix}`;
  const digest = await sha256Text(saltSource);
  return digest.slice(0, 16);
}

async function deriveMasterSecretArgon2id({ login, password, onProgress }) {
  const normalizedLogin = normalizeLoginForKdf(login);
  const normalizedPassword = String(password ?? '');
  const salt = await makeArgon2Salt(normalizedLogin, 'master.secret');
  const passBytes = utf8Bytes(`${normalizedLogin}\n${normalizedPassword}`);
  const out = await argon2idAsync(passBytes, salt, {
    t: 2,
    m: 65536,
    p: 1,
    dkLen: 32,
    onProgress,
  });
  return new Uint8Array(out);
}

function ed25519Pkcs8FromSeed(seed32) {
  if (seed32.length !== 32) {
    throw new Error('Для Ed25519 нужен seed длиной 32 байта');
  }
  const prefix = new Uint8Array([
    0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
  ]);
  const out = new Uint8Array(prefix.length + seed32.length);
  out.set(prefix, 0);
  out.set(seed32, prefix.length);
  return out;
}

export async function deriveMasterSecretFromPassword(password, options = {}) {
  const normalizedPassword = String(password ?? '');
  const normalizedLogin = String(options?.login ?? '');
  const onProgress = typeof options?.onProgress === 'function' ? options.onProgress : undefined;
  if (normalizedPassword.length === 0) {
    // Пустой пароль запрещён: упрощённый легаси-путь убран, регистрация/вход требуют непустой пароль.
    throw new Error('Пустой пароль запрещён: регистрация и вход требуют непустой пароль.');
  }
  return deriveMasterSecretArgon2id({
    login: normalizedLogin,
    password: normalizedPassword,
    onProgress,
  });
}

export async function deriveOpaqueArgon2Hash(password, options = {}) {
  const normalizedPassword = String(password ?? '');
  const normalizedLogin = String(options?.login ?? '');
  const normalizedSuffix = String(options?.suffix || 'opaque.hash');
  const salt = await makeArgon2Salt(normalizedLogin, normalizedSuffix);
  const passBytes = utf8Bytes(`${normalizeLoginForKdf(normalizedLogin)}\n${normalizedPassword}`);
  const out = await argon2idAsync(passBytes, salt, {
    t: 2,
    m: 65536,
    p: 1,
    dkLen: 32,
  });
  return `argon2id$v=19$m=65536,t=2,p=1$${bytesToBase64(salt)}$${bytesToBase64(new Uint8Array(out))}`;
}

export async function deriveEd25519FromMasterSecret(masterSecret32, suffix) {
  const secretBytes = masterSecret32 instanceof Uint8Array
    ? masterSecret32
    : new Uint8Array(masterSecret32 || []);
  if (secretBytes.length !== 32) {
    throw new Error('Master secret должен быть длиной 32 байта');
  }
  const material = `${bytesToBase64(secretBytes)}|${String(suffix || '')}`;
  const seed = await sha256Text(material);
  const pkcs8 = ed25519Pkcs8FromSeed(seed);
  const subtle = getSubtleApi();
  const privateKey = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
  const jwk = await subtle.exportKey('jwk', privateKey);
  if (!jwk.x) throw new Error('Не удалось получить публичный ключ Ed25519');
  return {
    privateKey,
    publicKeyB64: bytesToBase64(base64ToBytes(base64UrlToBase64(jwk.x))),
    privatePkcs8B64: bytesToBase64(pkcs8),
  };
}

export async function deriveAesKeyFromStoragePwd(storagePwd, saltBytes) {
  const subtle = getSubtleApi();
  const baseKey = await subtle.importKey(
    'raw',
    utf8Bytes(storagePwd),
    { name: 'PBKDF2' },
    false,
    ['deriveKey'],
  );

  return subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: saltBytes,
      iterations: 210000,
      hash: 'SHA-256',
    },
    baseKey,
    {
      name: 'AES-GCM',
      length: 256,
    },
    false,
    ['encrypt', 'decrypt'],
  );
}

export async function encryptJsonWithStoragePwd(value, storagePwd) {
  const cryptoApi = getCryptoApi();
  const subtle = getSubtleApi();
  const salt = cryptoApi.getRandomValues(new Uint8Array(16));
  const iv = cryptoApi.getRandomValues(new Uint8Array(12));
  const key = await deriveAesKeyFromStoragePwd(storagePwd, salt);
  const plainBytes = utf8Bytes(JSON.stringify(value));
  const cipher = await subtle.encrypt({ name: 'AES-GCM', iv }, key, plainBytes);

  return {
    saltB64: bytesToBase64(salt),
    ivB64: bytesToBase64(iv),
    cipherB64: bytesToBase64(new Uint8Array(cipher)),
  };
}

export async function decryptJsonWithStoragePwd(envelope, storagePwd) {
  const salt = base64ToBytes(envelope.saltB64);
  const iv = base64ToBytes(envelope.ivB64);
  const cipher = base64ToBytes(envelope.cipherB64);
  const key = await deriveAesKeyFromStoragePwd(storagePwd, salt);
  const plain = await getSubtleApi().decrypt({ name: 'AES-GCM', iv }, key, cipher);
  const text = new TextDecoder().decode(plain);
  return JSON.parse(text);
}

export async function generateEd25519Pair() {
  return getSubtleApi().generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
}

export async function exportEd25519PublicKeyB64(publicKey) {
  const raw = await getSubtleApi().exportKey('raw', publicKey);
  return bytesToBase64(new Uint8Array(raw));
}

export async function exportPkcs8B64(privateKey) {
  const raw = await getSubtleApi().exportKey('pkcs8', privateKey);
  return bytesToBase64(new Uint8Array(raw));
}

export async function importPkcs8Ed25519(pkcs8B64) {
  return getSubtleApi().importKey('pkcs8', base64ToBytes(pkcs8B64), { name: 'Ed25519' }, false, ['sign']);
}

export async function publicKeyB64FromPkcs8Ed25519(pkcs8B64) {
  const privateKey = await getSubtleApi().importKey('pkcs8', base64ToBytes(pkcs8B64), { name: 'Ed25519' }, true, ['sign']);
  const jwk = await getSubtleApi().exportKey('jwk', privateKey);
  if (!jwk.x) throw new Error('Не удалось получить публичный ключ Ed25519');
  return bytesToBase64(base64ToBytes(base64UrlToBase64(jwk.x)));
}

export async function signBase64(privateKey, text) {
  const signature = await getSubtleApi().sign({ name: 'Ed25519' }, privateKey, utf8Bytes(text));
  return bytesToBase64(new Uint8Array(signature));
}

export async function signBytes(privateKey, bytes) {
  const signature = await getSubtleApi().sign({ name: 'Ed25519' }, privateKey, bytes);
  return new Uint8Array(signature);
}
