import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
import {
  SHINE_LOGIN_GUARD_PROGRAM_ID,
  SHINE_PAYMENTS_PROGRAM_ID,
  SHINE_USERS_ECONOMY_CONFIG_SEED,
  SHINE_USERS_PROGRAM_ID,
} from '../solana-programs.js';

const MAGIC = 'SHiNE';
const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK';
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault';
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
const LIMIT_STEP = 10_000n;
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';

const BLOCK_TYPE_ROOT_KEY = 1;
const BLOCK_TYPE_DEVICE_KEY = 2;
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
const BLOCK_TYPE_SERVER_PROFILE = 30;
const BLOCK_TYPE_ACCESS_SERVERS = 40;
const BLOCK_TYPE_SESSIONS = 50;
const BLOCK_TYPE_TRUSTED_STATE = 70;
const SESSIONS_MODE_MIXED = 1;
const SESSION_TYPE_USER = 1;
const SESSION_TYPE_HOMESERVER = 100;

let solanaLibPromise = null;
function loadSolanaLib() {
  if (!solanaLibPromise) solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
  return solanaLibPromise;
}

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

function pushU32LE(buf, value) {
  const n = Number(value) >>> 0;
  buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
}

function pushU64LE(buf, value) {
  const b = BigInt(value);
  const lo = Number(b & 0xffffffffn) >>> 0;
  const hi = Number((b >> 32n) & 0xffffffffn) >>> 0;
  pushU32LE(buf, lo);
  pushU32LE(buf, hi);
}

function pushStrU8(buf, value) {
  const bytes = new TextEncoder().encode(String(value || ''));
  if (bytes.length > 255) throw new Error('Слишком длинная строка для формата U8');
  buf.push(bytes.length);
  for (const x of bytes) buf.push(x);
}

function pushStrU32(buf, value) {
  const bytes = new TextEncoder().encode(String(value || ''));
  pushU32LE(buf, bytes.length);
  for (const x of bytes) buf.push(x);
}

function pushVecU8(buf, bytes) {
  const data = bytes || new Uint8Array();
  pushU32LE(buf, data.length);
  for (const x of data) buf.push(x);
}

function pushVecStrU32(buf, values) {
  const arr = Array.isArray(values) ? values : [];
  pushU32LE(buf, arr.length);
  for (const value of arr) pushStrU32(buf, value);
}

function pushSessionRecordsU32(buf, sessions) {
  const arr = Array.isArray(sessions) ? sessions : [];
  pushU32LE(buf, arr.length);
  for (const session of arr) {
    buf.push(Number(session?.sessionType || 0) & 0xff);
    buf.push(Number(session?.sessionVersion || 0) & 0xff);
    pushStrU32(buf, String(session?.sessionName || ''));
    const key = session?.sessionPubKey32 || new Uint8Array(32);
    for (const x of key) buf.push(x);
  }
}

function makeReader(bytes) {
  let offset = 0;
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const ensure = (len) => {
    if (offset + len > bytes.length) throw new Error('Повреждённый формат PDA');
  };
  const readU8 = () => {
    ensure(1);
    const value = view.getUint8(offset);
    offset += 1;
    return value;
  };
  const readU16 = () => {
    ensure(2);
    const value = view.getUint16(offset, true);
    offset += 2;
    return value;
  };
  const readU32 = () => {
    ensure(4);
    const value = view.getUint32(offset, true);
    offset += 4;
    return value;
  };
  const readU64 = () => {
    ensure(8);
    const value = view.getBigUint64(offset, true);
    offset += 8;
    return value;
  };
  const readBytes = (len) => {
    ensure(len);
    const out = bytes.slice(offset, offset + len);
    offset += len;
    return out;
  };
  const readStrU8 = () => {
    const len = readU8();
    return new TextDecoder().decode(readBytes(len));
  };
  return {
    readU8,
    readU16,
    readU32,
    readU64,
    readBytes,
    readStrU8,
    get offset() {
      return offset;
    },
  };
}

function parseUsersEconomyConfig(dataBytes) {
  const view = new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength);
  if (dataBytes.byteLength < 25) throw new Error('Некорректный economy config');
  return {
    version: view.getUint8(0),
    registrationFeeLamports: view.getBigUint64(1, true),
    lamportsPerLimitStep: view.getBigUint64(9, true),
    startBonusLimit: view.getBigUint64(17, true),
  };
}

function buildEd25519IxData(sig64, pubkey32, msgHash32) {
  const sigOff = 16;
  const pkOff = sigOff + 64;
  const msgOff = pkOff + 32;
  const data = new Uint8Array(msgOff + 32);
  const view = new DataView(data.buffer);
  data[0] = 1;
  data[1] = 0;
  view.setUint16(2, sigOff, true);
  view.setUint16(4, 0xffff, true);
  view.setUint16(6, pkOff, true);
  view.setUint16(8, 0xffff, true);
  view.setUint16(10, msgOff, true);
  view.setUint16(12, 32, true);
  view.setUint16(14, 0xffff, true);
  data.set(sig64, sigOff);
  data.set(pubkey32, pkOff);
  data.set(msgHash32, msgOff);
  return data;
}

function serializeCreateUserPdaArgs(args) {
  const buf = [];
  buf.push(3);
  pushStrU8(buf, args.login);
  for (const x of args.rootKey32) buf.push(x);
  pushU64LE(buf, args.createdAtMs);
  pushU64LE(buf, BigInt(args.additionalLimitBytes || 0n));
  for (const x of args.deviceKey32) buf.push(x);
  for (const x of args.blockchainPublicKey32) buf.push(x);
  pushStrU8(buf, args.blockchainName);
  pushU64LE(buf, args.usedBytes);
  pushU32LE(buf, args.lastBlockNumber);
  for (const x of args.lastBlockHash32) buf.push(x);
  for (const x of args.lastBlockSignature64) buf.push(x);
  pushStrU8(buf, args.arweaveTxId);
  buf.push(args.isServer ? 1 : 0);
  if (args.isServer) {
    buf.push(Number(args.addressFormatType || 0) & 0xff);
    buf.push(Number(args.addressFormatVersion || 0) & 0xff);
    pushStrU8(buf, args.serverAddress);
    const syncServers = Array.isArray(args.syncServers) ? args.syncServers : [];
    buf.push(syncServers.length & 0xff);
    for (const value of syncServers) pushStrU8(buf, value);
  }
  const accessServers = Array.isArray(args.accessServers) ? args.accessServers : [];
  buf.push(accessServers.length & 0xff);
  for (const value of accessServers) pushStrU8(buf, value);
  buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
  const sessions = Array.isArray(args.sessions) ? args.sessions : [];
  buf.push(sessions.length & 0xff);
  for (const session of sessions) {
    buf.push(Number(session?.sessionType || 0) & 0xff);
    buf.push(Number(session?.sessionVersion || 0) & 0xff);
    pushStrU8(buf, String(session?.sessionName || ''));
    const key = session?.sessionPubKey32 || new Uint8Array(32);
    for (const x of key) buf.push(x);
  }
  buf.push(Number(args.trustedCount || 0) & 0xff);
  for (const x of args.rootSignature64) buf.push(x);
  return new Uint8Array(buf);
}

function serializeUpdateUserPdaArgs(args) {
  const buf = [];
  buf.push(4);
  pushStrU8(buf, args.login);
  for (const x of args.rootKey32) buf.push(x);
  pushU64LE(buf, args.createdAtMs);
  pushU64LE(buf, args.updatedAtMs);
  pushU32LE(buf, args.version);
  for (const x of args.prevHash32) buf.push(x);
  pushU64LE(buf, args.additionalLimitBytes);
  for (const x of args.deviceKey32) buf.push(x);
  for (const x of args.blockchainPublicKey32) buf.push(x);
  pushStrU8(buf, args.blockchainName);
  pushU64LE(buf, args.usedBytes);
  pushU32LE(buf, args.lastBlockNumber);
  for (const x of args.lastBlockHash32) buf.push(x);
  for (const x of args.lastBlockSignature64) buf.push(x);
  pushStrU8(buf, args.arweaveTxId);
  buf.push(args.isServer ? 1 : 0);
  if (args.isServer) {
    buf.push(Number(args.addressFormatType || 0) & 0xff);
    buf.push(Number(args.addressFormatVersion || 0) & 0xff);
    pushStrU8(buf, args.serverAddress);
    const syncServers = Array.isArray(args.syncServers) ? args.syncServers : [];
    buf.push(syncServers.length & 0xff);
    for (const value of syncServers) pushStrU8(buf, value);
  }
  const accessServers = Array.isArray(args.accessServers) ? args.accessServers : [];
  buf.push(accessServers.length & 0xff);
  for (const value of accessServers) pushStrU8(buf, value);
  buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
  const sessions = Array.isArray(args.sessions) ? args.sessions : [];
  buf.push(sessions.length & 0xff);
  for (const session of sessions) {
    buf.push(Number(session?.sessionType || 0) & 0xff);
    buf.push(Number(session?.sessionVersion || 0) & 0xff);
    pushStrU8(buf, String(session?.sessionName || ''));
    const key = session?.sessionPubKey32 || new Uint8Array(32);
    for (const x of key) buf.push(x);
  }
  buf.push(Number(args.trustedCount || 0) & 0xff);
  for (const x of args.rootSignature64) buf.push(x);
  return new Uint8Array(buf);
}

function createBlockchainState({
  blockchainName,
  blockchainPublicKey,
  paidLimitBytes,
  usedBytes,
  lastBlockNumber,
  lastBlockHash,
  lastBlockSignature,
  arweaveTxId,
}) {
  return {
    blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
    blockchainName,
    blockchainPublicKey,
    paidLimitBytes,
    usedBytes,
    lastBlockNumber,
    lastBlockHash,
    lastBlockSignature,
    arweaveTxId,
  };
}

function createPdaState({
  login,
  createdAtMs,
  updatedAtMs,
  recordNumber,
  prevRecordHash,
  rootKey,
  deviceKey,
  blockchain,
  isServer,
  addressFormatType,
  addressFormatVersion,
  serverAddress,
  syncServers,
  accessServers,
  sessionsMode,
  sessions,
  trustedCount,
}) {
  const serverProfile = isServer ? {
    addressFormatType: Number(addressFormatType || 0),
    addressFormatVersion: Number(addressFormatVersion || 0),
    serverAddress: String(serverAddress || ''),
    syncServers: Array.isArray(syncServers) ? [...syncServers] : [],
  } : null;
  return {
    createdAtMs,
    updatedAtMs,
    recordNumber,
    prevRecordHash,
    login,
    rootKey,
    deviceKey,
    blockchain,
    isServer: Boolean(isServer),
    serverProfile,
    serverData: serverProfile,
    addressFormatType: serverProfile?.addressFormatType ?? 0,
    addressFormatVersion: serverProfile?.addressFormatVersion ?? 0,
    serverAddress: serverProfile?.serverAddress ?? '',
    syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [],
    accessServers: Array.isArray(accessServers) ? [...accessServers] : [],
    sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff,
    sessions: Array.isArray(sessions) ? sessions.map((x) => ({
      sessionType: Number(x?.sessionType || 0) & 0xff,
      sessionVersion: Number(x?.sessionVersion || 0) & 0xff,
      sessionName: String(x?.sessionName || ''),
      sessionPubKey32: x?.sessionPubKey32 instanceof Uint8Array ? x.sessionPubKey32 : new Uint8Array(x?.sessionPubKey32 || 32),
    })) : [],
    trustedCount: Number(trustedCount || 0) & 0xff,
  };
}

function encodeOptionalArweave(buf, arweaveTxId) {
  const value = String(arweaveTxId || '').trim();
  if (value) {
    buf.push(1);
    pushStrU8(buf, value);
  } else {
    buf.push(0);
  }
}

export function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber = 0, lastBlockHash32 = new Uint8Array(32), usedBytes = 0n) {
  const buf = [];
  for (const x of new TextEncoder().encode(LAST_BLOCK_STATE_PREFIX)) buf.push(x);
  pushStrU8(buf, login);
  pushStrU8(buf, blockchainName);
  pushU32LE(buf, lastBlockNumber);
  for (const x of lastBlockHash32) buf.push(x);
  pushU64LE(buf, usedBytes);
  return new Uint8Array(buf);
}

export function parseShineUserPda(dataBytes) {
  const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []);
  const reader = makeReader(bytes);
  const magic = new TextDecoder().decode(reader.readBytes(5));
  if (magic !== MAGIC) throw new Error('Некорректный формат PDA');
  reader.readU8();
  reader.readU8();
  const recordLen = reader.readU16();
  if (recordLen < 9 + 64 || recordLen > bytes.length) throw new Error('Некорректный record_len');

  const createdAtMs = reader.readU64();
  const updatedAtMs = reader.readU64();
  const recordNumber = reader.readU32();
  const prevRecordHash = reader.readBytes(32);
  const login = reader.readStrU8();
  const blocksCount = reader.readU8();

  let rootKey = null;
  let deviceKey = null;
  let blockchain = null;
  let isServer = false;
  let addressFormatType = 0;
  let addressFormatVersion = 0;
  let serverAddress = '';
  let syncServers = [];
  let accessServers = [];
  let sessionsMode = SESSIONS_MODE_MIXED;
  let sessions = [];
  let trustedCount = 0;

  for (let i = 0; i < blocksCount; i += 1) {
    const blockType = reader.readU8();
    reader.readU8();

    if (blockType === BLOCK_TYPE_ROOT_KEY) {
      rootKey = reader.readBytes(32);
      continue;
    }
    if (blockType === BLOCK_TYPE_DEVICE_KEY) {
      deviceKey = reader.readBytes(32);
      continue;
    }
    if (blockType === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) {
      const count = reader.readU8();
      for (let j = 0; j < count; j += 1) {
        const blockchainType = reader.readU8();
        const blockchainName = reader.readStrU8();
        const blockchainPublicKey = reader.readBytes(32);
        const paidLimitBytes = reader.readU64();
        const usedBytes = reader.readU64();
        const lastBlockNumber = reader.readU32();
        const lastBlockHash = reader.readBytes(32);
        const lastBlockSignature = reader.readBytes(64);
        const arweavePresent = reader.readU8();
        const arweaveTxId = arweavePresent === 1 ? reader.readStrU8() : '';
        if (!blockchain) {
          blockchain = {
            blockchainType,
            blockchainName,
            blockchainPublicKey,
            paidLimitBytes,
            usedBytes,
            lastBlockNumber,
            lastBlockHash,
            lastBlockSignature,
            arweaveTxId,
          };
        }
      }
      continue;
    }
    if (blockType === BLOCK_TYPE_SERVER_PROFILE) {
      isServer = reader.readU8() === 1;
      if (isServer) {
        addressFormatType = reader.readU8();
        addressFormatVersion = reader.readU8();
        serverAddress = reader.readStrU8();
        const syncCount = reader.readU8();
        syncServers = [];
        for (let j = 0; j < syncCount; j += 1) syncServers.push(reader.readStrU8());
      }
      continue;
    }
    if (blockType === BLOCK_TYPE_ACCESS_SERVERS) {
      const accessCount = reader.readU8();
      accessServers = [];
      for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8());
      continue;
    }
    if (blockType === BLOCK_TYPE_SESSIONS) {
      sessionsMode = reader.readU8();
      const sessionsCount = reader.readU8();
      sessions = [];
      for (let j = 0; j < sessionsCount; j += 1) {
        sessions.push({
          sessionType: reader.readU8(),
          sessionVersion: reader.readU8(),
          sessionName: reader.readStrU8(),
          sessionPubKey32: reader.readBytes(32),
        });
      }
      continue;
    }
    if (blockType === BLOCK_TYPE_TRUSTED_STATE) {
      trustedCount = reader.readU8();
      continue;
    }
    throw new Error(`Неизвестный блок PDA: ${blockType}`);
  }

  if (!rootKey || !deviceKey || !blockchain) {
    throw new Error('В PDA отсутствуют обязательные блоки');
  }

  const signature = bytes.slice(reader.offset, reader.offset + 64);
  const unsignedBytes = bytes.slice(0, recordLen - 64);
  const state = createPdaState({
    login,
    createdAtMs,
    updatedAtMs,
    recordNumber,
    prevRecordHash,
    rootKey,
    deviceKey,
    blockchain,
    isServer,
    addressFormatType,
    addressFormatVersion,
    serverAddress,
    syncServers,
    accessServers,
    sessionsMode,
    sessions,
    trustedCount,
  });

  return {
    ...state,
    recordLen,
    unsignedBytes,
    signature,
  };
}

export function serializeUnsignedRecordFromState(stateLike) {
  const state = createPdaState({
    login: stateLike.login,
    createdAtMs: stateLike.createdAtMs,
    updatedAtMs: stateLike.updatedAtMs,
    recordNumber: stateLike.recordNumber,
    prevRecordHash: stateLike.prevRecordHash,
    rootKey: stateLike.rootKey,
    deviceKey: stateLike.deviceKey,
    blockchain: stateLike.blockchain,
    isServer: stateLike.isServer,
    addressFormatType: stateLike.addressFormatType ?? stateLike.serverProfile?.addressFormatType,
    addressFormatVersion: stateLike.addressFormatVersion ?? stateLike.serverProfile?.addressFormatVersion,
    serverAddress: stateLike.serverAddress ?? stateLike.serverProfile?.serverAddress,
    syncServers: stateLike.syncServers ?? stateLike.serverProfile?.syncServers,
    accessServers: stateLike.accessServers,
    sessionsMode: stateLike.sessionsMode,
    sessions: stateLike.sessions,
    trustedCount: stateLike.trustedCount,
  });

  const buf = [0x53, 0x48, 0x69, 0x4e, 0x45, 1, 0, 0, 0];
  pushU64LE(buf, state.createdAtMs);
  pushU64LE(buf, state.updatedAtMs);
  pushU32LE(buf, state.recordNumber);
  for (const x of state.prevRecordHash) buf.push(x);
  pushStrU8(buf, state.login);
  buf.push(state.isServer ? 7 : 6);

  buf.push(BLOCK_TYPE_ROOT_KEY, 0);
  for (const x of state.rootKey) buf.push(x);

  buf.push(BLOCK_TYPE_DEVICE_KEY, 0);
  for (const x of state.deviceKey) buf.push(x);

  buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1, state.blockchain.blockchainType);
  pushStrU8(buf, state.blockchain.blockchainName);
  for (const x of state.blockchain.blockchainPublicKey) buf.push(x);
  pushU64LE(buf, state.blockchain.paidLimitBytes);
  pushU64LE(buf, state.blockchain.usedBytes);
  pushU32LE(buf, state.blockchain.lastBlockNumber);
  for (const x of state.blockchain.lastBlockHash) buf.push(x);
  for (const x of state.blockchain.lastBlockSignature) buf.push(x);
  encodeOptionalArweave(buf, state.blockchain.arweaveTxId);

  if (state.isServer) {
    buf.push(BLOCK_TYPE_SERVER_PROFILE, 0, 1);
    buf.push(state.addressFormatType & 0xff);
    buf.push(state.addressFormatVersion & 0xff);
    pushStrU8(buf, state.serverAddress);
    buf.push(state.syncServers.length & 0xff);
    for (const loginValue of state.syncServers) pushStrU8(buf, loginValue);
  }

  buf.push(BLOCK_TYPE_ACCESS_SERVERS, 0, state.accessServers.length & 0xff);
  for (const loginValue of state.accessServers) pushStrU8(buf, loginValue);

  buf.push(BLOCK_TYPE_SESSIONS, 0, state.sessionsMode & 0xff, state.sessions.length & 0xff);
  for (const session of state.sessions) {
    buf.push(session.sessionType & 0xff, session.sessionVersion & 0xff);
    pushStrU8(buf, session.sessionName);
    for (const x of session.sessionPubKey32) buf.push(x);
  }

  buf.push(BLOCK_TYPE_TRUSTED_STATE, 0, state.trustedCount & 0xff);

  const recordLen = buf.length + 64;
  buf[7] = recordLen & 0xff;
  buf[8] = (recordLen >>> 8) & 0xff;
  return new Uint8Array(buf);
}

export async function getShineUsersEconomyConfig({ solanaEndpoint }) {
  const endpoint = String(solanaEndpoint || '').trim();
  if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
  const solana = await loadSolanaLib();
  const connection = new solana.Connection(endpoint, 'confirmed');
  const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
  const [economyPda] = solana.PublicKey.findProgramAddressSync(
    [new TextEncoder().encode(SHINE_USERS_ECONOMY_CONFIG_SEED)],
    usersProgram,
  );
  const accountInfo = await connection.getAccountInfo(economyPda, 'confirmed');
  if (!accountInfo?.data) throw new Error('Economy config PDA не найден');
  return { endpoint, economyPda: economyPda.toBase58(), ...parseUsersEconomyConfig(accountInfo.data) };
}

export async function readShineUserPda({ login, solanaEndpoint }) {
  const cleanLogin = normalizeLogin(login);
  const endpoint = String(solanaEndpoint || '').trim();
  if (!cleanLogin) throw new Error('Не указан логин');
  if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
  const solana = await loadSolanaLib();
  const connection = new solana.Connection(endpoint, 'confirmed');
  const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
  const enc = new TextEncoder();
  const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram);
  const accountInfo = await connection.getAccountInfo(userPda, 'confirmed');
  if (!accountInfo?.data) throw new Error(`PDA не найдена для логина «${cleanLogin}»`);
  return {
    ...parseShineUserPda(accountInfo.data),
    userPda: userPda.toBase58(),
    pdaAddress: userPda.toBase58(),
    endpoint,
  };
}

export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
  const parsed = await readShineUserPda({ login, solanaEndpoint });
  const bch = parsed.blockchain;
  const leftBytes = bch.paidLimitBytes > bch.usedBytes ? (bch.paidLimitBytes - bch.usedBytes) : 0n;
  return {
    endpoint: parsed.endpoint,
    userPda: parsed.userPda,
    login: parsed.login,
    recordNumber: parsed.recordNumber,
    paidLimitBytes: bch.paidLimitBytes,
    usedBytes: bch.usedBytes,
    leftBytes,
    lastBlockNumber: bch.lastBlockNumber,
    lastBlockHashHex: Array.from(bch.lastBlockHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
  };
}

function parseHex32(value) {
  const clean = String(value || '').trim().toLowerCase();
  if (!clean) return null;
  if (!/^[0-9a-f]+$/.test(clean) || clean.length !== 64) {
    throw new Error('last block hash должен быть 32 байта в hex');
  }
  const out = new Uint8Array(32);
  for (let i = 0; i < 32; i += 1) {
    out[i] = parseInt(clean.slice(i * 2, (i * 2) + 2), 16);
  }
  return out;
}

async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
  const cleanLogin = normalizeLogin(login);
  const endpoint = String(solanaEndpoint || '').trim();
  if (!cleanLogin) throw new Error('Не указан логин');
  if (!endpoint) throw new Error('Не указан Solana RPC endpoint');

  const solana = await loadSolanaLib();
  const connection = new solana.Connection(endpoint, 'confirmed');
  const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
  const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
  const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
  const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
  const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
  const enc = new TextEncoder();

  const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram);
  const [economyConfigPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
  const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);

  const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
  const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
  const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);

  const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
  const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
  const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
  const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);

  return {
    cleanLogin,
    endpoint,
    solana,
    connection,
    usersProgram,
    paymentsProgram,
    loginGuardProgram,
    ed25519Program,
    sysvarInstructions,
    userPda,
    economyConfigPda,
    inflowVault,
    rootKey32,
    blockchainKey32,
    deviceKey32,
    rootPrivKey,
    bchPrivKey,
    deviceKeypair,
  };
}

async function createShineUserPdaOnSolana({
  login,
  keyBundle,
  solanaEndpoint,
  isServer = false,
  addressFormatType = 0,
  addressFormatVersion = 0,
  serverAddress = '',
  syncServers = [],
  accessServers = [],
}) {
  const ctx = await buildCreateContext({ login, keyBundle, solanaEndpoint });
  const ecoAccount = await ctx.connection.getAccountInfo(ctx.economyConfigPda);
  if (!ecoAccount?.data) {
    throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.');
  }

  const cleanLogin = ctx.cleanLogin;
  const blockchainName = `${cleanLogin}-001`;
  const zeroHash32 = new Uint8Array(32);
  const createdAtMs = BigInt(Date.now());
  const startBonusLimit = parseUsersEconomyConfig(ecoAccount.data).startBonusLimit;

  const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, zeroHash32, 0n);
  const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
  const lastBlockSig64 = await signBytes(ctx.bchPrivKey, lastBlockStateHash);

  const initialState = createPdaState({
    login: cleanLogin,
    createdAtMs,
    updatedAtMs: createdAtMs,
    recordNumber: 0,
    prevRecordHash: zeroHash32,
    rootKey: ctx.rootKey32,
    deviceKey: ctx.deviceKey32,
    blockchain: createBlockchainState({
      blockchainName,
      blockchainPublicKey: ctx.blockchainKey32,
      paidLimitBytes: startBonusLimit,
      usedBytes: 0n,
      lastBlockNumber: 0,
      lastBlockHash: zeroHash32,
      lastBlockSignature: lastBlockSig64,
      arweaveTxId: '',
    }),
    isServer,
    addressFormatType,
    addressFormatVersion,
    serverAddress,
    syncServers,
    accessServers,
    sessionsMode: SESSIONS_MODE_MIXED,
    sessions: [],
    trustedCount: 0,
  });

  const unsignedRecord = serializeUnsignedRecordFromState(initialState);
  const unsignedHash = await sha256Bytes(unsignedRecord);
  const rootSig64 = await signBytes(ctx.rootPrivKey, unsignedHash);

  const ixData = serializeCreateUserPdaArgs({
    login: cleanLogin,
    rootKey32: ctx.rootKey32,
    createdAtMs,
    additionalLimitBytes: 0n,
    deviceKey32: ctx.deviceKey32,
    blockchainPublicKey32: ctx.blockchainKey32,
    blockchainName,
    usedBytes: 0n,
    lastBlockNumber: 0,
    lastBlockHash32: zeroHash32,
    lastBlockSignature64: lastBlockSig64,
    arweaveTxId: '',
    isServer,
    addressFormatType: isServer ? addressFormatType : 0,
    addressFormatVersion: isServer ? addressFormatVersion : 0,
    serverAddress: isServer ? serverAddress : '',
    syncServers: isServer ? syncServers : [],
    accessServers,
    sessionsMode: SESSIONS_MODE_MIXED,
    sessions: [],
    trustedCount: 0,
    rootSignature64: rootSig64,
  });

  const ed25519RootIx = new ctx.solana.TransactionInstruction({
    programId: ctx.ed25519Program,
    keys: [],
    data: buildEd25519IxData(rootSig64, ctx.rootKey32, unsignedHash),
  });
  const ed25519BchIx = new ctx.solana.TransactionInstruction({
    programId: ctx.ed25519Program,
    keys: [],
    data: buildEd25519IxData(lastBlockSig64, ctx.blockchainKey32, lastBlockStateHash),
  });
  const createIx = new ctx.solana.TransactionInstruction({
    programId: ctx.usersProgram,
    keys: [
      { pubkey: ctx.deviceKeypair.publicKey, isSigner: true, isWritable: true },
      { pubkey: ctx.userPda, isSigner: false, isWritable: true },
      { pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
      { pubkey: ctx.sysvarInstructions, isSigner: false, isWritable: false },
      { pubkey: ctx.economyConfigPda, isSigner: false, isWritable: false },
      { pubkey: ctx.loginGuardProgram, isSigner: false, isWritable: false },
    ],
    data: ixData,
  });

  const signature = await ctx.solana.sendAndConfirmTransaction(
    ctx.connection,
    new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
    [ctx.deviceKeypair],
    { commitment: 'confirmed' },
  );

  return {
    signature,
    userPda: ctx.userPda.toBase58(),
    pdaAddress: ctx.userPda.toBase58(),
    blockchainName,
  };
}

export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint }) {
  return createShineUserPdaOnSolana({
    login,
    keyBundle,
    solanaEndpoint,
    isServer: false,
    accessServers: ['shineup.me'],
  });
}

export async function registerServerOnSolana({
  login,
  keyBundle,
  serverAddress,
  addressFormatType = 1,
  addressFormatVersion = 0,
  syncServers = [],
  accessServers = [],
  solanaEndpoint,
}) {
  return createShineUserPdaOnSolana({
    login,
    keyBundle,
    solanaEndpoint,
    isServer: true,
    addressFormatType,
    addressFormatVersion,
    serverAddress,
    syncServers,
    accessServers,
  });
}

export async function updateShineUserPdaOnSolana({
  login,
  solanaEndpoint,
  rootPrivatePkcs8B64,
  devicePrivatePkcs8B64,
  blockchainPrivatePkcs8B64,
  additionalLimitBytes = 0n,
  nextUsedBytes,
  nextLastBlockNumber,
  nextLastBlockHashHex,
  serverProfile,
  accessServers,
  trustedCount,
}) {
  const current = await readShineUserPda({ login, solanaEndpoint });
  const cleanLogin = current.login;
  const endpoint = current.endpoint;
  const currentBch = current.blockchain;
  const addLimit = BigInt(additionalLimitBytes || 0);
  if (addLimit < 0n) throw new Error('Нельзя уменьшать лимит');
  if (addLimit % LIMIT_STEP !== 0n) throw new Error(`Лимит можно увеличивать только шагом ${LIMIT_STEP}`);

  const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes);
  const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber);
  const effectiveLastHash = parseHex32(nextLastBlockHashHex) || currentBch.lastBlockHash;
  if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта');

  const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64);
  const solana = await loadSolanaLib();
  const connection = new solana.Connection(endpoint, 'confirmed');
  const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
  const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
  const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
  const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
  const enc = new TextEncoder();
  const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram);
  const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
  const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
  const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64);
  const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);

  const lastBlockStateBytes = buildLastBlockStateBytes(
    cleanLogin,
    currentBch.blockchainName,
    effectiveLastNum,
    effectiveLastHash,
    effectiveUsed,
  );
  const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);

  const blockchainStateWillChange = (
    nextUsedBytes != null
    || nextLastBlockNumber != null
    || nextLastBlockHashHex != null
  );

  let lastBlockSig64 = currentBch.lastBlockSignature;
  if (blockchainPrivatePkcs8B64) {
    const bchPriv = await importPkcs8Ed25519(blockchainPrivatePkcs8B64);
    lastBlockSig64 = await signBytes(bchPriv, lastBlockStateHash);
  } else if (blockchainStateWillChange) {
    throw new Error('Для изменения last block state нужен blockchain-ключ');
  }

  const updatedAtMs = BigInt(Date.now());
  const newPaid = currentBch.paidLimitBytes + addLimit;
  const newRecordNumber = current.recordNumber + 1;
  const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current));

  const nextServerProfile = serverProfile
    ? {
      addressFormatType: Number(serverProfile.addressFormatType ?? current.addressFormatType ?? 0),
      addressFormatVersion: Number(serverProfile.addressFormatVersion ?? current.addressFormatVersion ?? 0),
      serverAddress: String(serverProfile.serverAddress ?? current.serverAddress ?? ''),
      syncServers: Array.isArray(serverProfile.syncServers) ? [...serverProfile.syncServers] : [...current.syncServers],
    }
    : current.serverProfile;

  const nextState = createPdaState({
    login: cleanLogin,
    createdAtMs: current.createdAtMs,
    updatedAtMs,
    recordNumber: newRecordNumber,
    prevRecordHash: prevHash,
    rootKey: current.rootKey,
    deviceKey: current.deviceKey,
    blockchain: createBlockchainState({
      blockchainName: currentBch.blockchainName,
      blockchainPublicKey: currentBch.blockchainPublicKey,
      paidLimitBytes: newPaid,
      usedBytes: effectiveUsed,
      lastBlockNumber: effectiveLastNum,
      lastBlockHash: effectiveLastHash,
      lastBlockSignature: lastBlockSig64,
      arweaveTxId: currentBch.arweaveTxId,
    }),
    isServer: Boolean(nextServerProfile),
    addressFormatType: nextServerProfile?.addressFormatType ?? 0,
    addressFormatVersion: nextServerProfile?.addressFormatVersion ?? 0,
    serverAddress: nextServerProfile?.serverAddress ?? '',
    syncServers: nextServerProfile?.syncServers ?? [],
    accessServers: accessServers == null ? current.accessServers : accessServers,
    sessionsMode: current.sessionsMode,
    sessions: current.sessions,
    trustedCount: trustedCount == null ? current.trustedCount : trustedCount,
  });

  const unsignedNext = serializeUnsignedRecordFromState(nextState);
  const unsignedNextHash = await sha256Bytes(unsignedNext);
  const rootSig64 = await signBytes(rootPriv, unsignedNextHash);

  const ixData = serializeUpdateUserPdaArgs({
    login: cleanLogin,
    rootKey32: current.rootKey,
    createdAtMs: current.createdAtMs,
    updatedAtMs,
    version: newRecordNumber,
    prevHash32: prevHash,
    additionalLimitBytes: addLimit,
    deviceKey32: current.deviceKey,
    blockchainPublicKey32: currentBch.blockchainPublicKey,
    blockchainName: currentBch.blockchainName,
    usedBytes: effectiveUsed,
    lastBlockNumber: effectiveLastNum,
    lastBlockHash32: effectiveLastHash,
    lastBlockSignature64: lastBlockSig64,
    arweaveTxId: currentBch.arweaveTxId,
    isServer: nextState.isServer,
    addressFormatType: nextState.addressFormatType,
    addressFormatVersion: nextState.addressFormatVersion,
    serverAddress: nextState.serverAddress,
    syncServers: nextState.syncServers,
    accessServers: nextState.accessServers,
    sessionsMode: nextState.sessionsMode,
    sessions: nextState.sessions,
    trustedCount: nextState.trustedCount,
    rootSignature64: rootSig64,
  });

  const edIxRoot = new solana.TransactionInstruction({
    programId: ed25519Program,
    keys: [],
    data: buildEd25519IxData(rootSig64, current.rootKey, unsignedNextHash),
  });
  const edIxBch = new solana.TransactionInstruction({
    programId: ed25519Program,
    keys: [],
    data: buildEd25519IxData(lastBlockSig64, currentBch.blockchainPublicKey, lastBlockStateHash),
  });
  const updateIx = new solana.TransactionInstruction({
    programId: usersProgram,
    keys: [
      { pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
      { pubkey: userPda, isSigner: false, isWritable: true },
      { pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: inflowVault, isSigner: false, isWritable: true },
      { pubkey: sysvarInstructions, isSigner: false, isWritable: false },
      { pubkey: economyPda, isSigner: false, isWritable: false },
    ],
    data: ixData,
  });
  const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
  const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });

  const signature = await solana.sendAndConfirmTransaction(
    connection,
    new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
    [deviceKeypair],
    { commitment: 'confirmed' },
  );

  return {
    signature,
    userPda: userPda.toBase58(),
    pdaAddress: userPda.toBase58(),
    paidLimitBytes: newPaid,
    usedBytes: effectiveUsed,
    leftBytes: newPaid > effectiveUsed ? (newPaid - effectiveUsed) : 0n,
    lastBlockNumber: effectiveLastNum,
    lastBlockHashHex: Array.from(effectiveLastHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
  };
}

export async function updateServerOnSolana({
  login,
  keyBundle,
  serverAddress,
  addressFormatType,
  addressFormatVersion,
  syncServers,
  solanaEndpoint,
}) {
  return updateShineUserPdaOnSolana({
    login,
    solanaEndpoint,
    rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
    devicePrivatePkcs8B64: keyBundle.devicePair.privatePkcs8B64,
    serverProfile: {
      addressFormatType,
      addressFormatVersion,
      serverAddress,
      syncServers,
    },
  });
}

export function calcLimitTopupPriceLamports(additionalLimitBytes, lamportsPerLimitStep) {
  const add = BigInt(additionalLimitBytes || 0);
  const pricePerStep = BigInt(lamportsPerLimitStep || 0);
  if (add < 0n) throw new Error('Некорректный размер увеличения лимита');
  if (add % LIMIT_STEP !== 0n) throw new Error(`Увеличение лимита должно быть кратно ${LIMIT_STEP} байт`);
  return (add / LIMIT_STEP) * pricePerStep;
}

export function getLimitStepBytes() {
  return LIMIT_STEP;
}
