import type { PriceUpdate, PythMessageParts } from './types.js';
import type { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk';
import {
  MARKET_SESSION_ORDER,
  PRICE_UPDATE_MAGIC,
  PROP_ID,
  SOLANA_FORMAT_MAGIC,
} from './types.js';

function expectMagic(
  buf: Uint8Array,
  offset: number,
  magic: Uint8Array,
  label: string,
): void {
  for (let i = 0; i < magic.length; i++) {
    if (buf[offset + i] !== magic[i]) {
      throw new Error(`Invalid ${label} magic byte at ${i}`);
    }
  }
}

function bigintToSdkNumber(v: bigint): number {
  if (
    v > BigInt(Number.MAX_SAFE_INTEGER) ||
    v < BigInt(Number.MIN_SAFE_INTEGER)
  ) {
    throw new RangeError('Scalar does not fit in a safe integer');
  }
  return Number(v);
}

function decodeFeedPayload(
  buf: Uint8Array,
  view: DataView,
  start: number,
): { feed: ParsedFeedPayload; next: number } {
  let o = start;
  if (o + 5 > buf.length) throw new RangeError('Truncated feed header');
  const priceFeedId = view.getUint32(o, true);
  o += 4;
  const nProps = buf[o];
  o += 1;
  const feed: ParsedFeedPayload = { priceFeedId };
  for (let i = 0; i < nProps; i++) {
    if (o >= buf.length) throw new RangeError('Truncated feed property header');
    const propId = buf[o];
    o += 1;
    switch (propId) {
      case PROP_ID.Price: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.price = v.toString();
        break;
      }
      case PROP_ID.BestBidPrice: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.bestBidPrice = v.toString();
        break;
      }
      case PROP_ID.BestAskPrice: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.bestAskPrice = v.toString();
        break;
      }
      case PROP_ID.PublisherCount: {
        feed.publisherCount = view.getUint16(o, true);
        o += 2;
        break;
      }
      case PROP_ID.Exponent: {
        feed.exponent = view.getInt16(o, true);
        o += 2;
        break;
      }
      case PROP_ID.Confidence: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.confidence = bigintToSdkNumber(v);
        break;
      }
      case PROP_ID.FundingRate: {
        const tag = buf[o];
        o += 1;
        if (tag === 1) {
          const v = view.getBigInt64(o, true);
          o += 8;
          feed.fundingRate = bigintToSdkNumber(v);
        } else if (tag !== 0) {
          throw new RangeError('Invalid FundingRate option tag');
        }
        break;
      }
      case PROP_ID.FundingTimestamp: {
        const tag = buf[o];
        o += 1;
        if (tag === 1) {
          const v = view.getBigUint64(o, true);
          o += 8;
          feed.fundingTimestamp = bigintToSdkNumber(v);
        } else if (tag !== 0) {
          throw new RangeError('Invalid FundingTimestamp option tag');
        }
        break;
      }
      case PROP_ID.FundingRateInterval: {
        const tag = buf[o];
        o += 1;
        if (tag === 1) {
          const v = view.getBigUint64(o, true);
          o += 8;
          feed.fundingRateInterval = bigintToSdkNumber(v);
        } else if (tag !== 0) {
          throw new RangeError('Invalid FundingRateInterval option tag');
        }
        break;
      }
      case PROP_ID.MarketSession: {
        const idx = view.getUint16(o, true);
        o += 2;
        const session = MARKET_SESSION_ORDER[idx];
        if (!session) throw new RangeError('Invalid marketSession index');
        feed.marketSession = session;
        break;
      }
      case PROP_ID.EmaPrice: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.emaPrice = v.toString();
        break;
      }
      case PROP_ID.EmaConfidence: {
        const v = view.getBigInt64(o, true);
        o += 8;
        if (v !== 0n) feed.emaConfidence = bigintToSdkNumber(v);
        break;
      }
      case PROP_ID.FeedUpdateTimestamp: {
        const tag = buf[o];
        o += 1;
        if (tag === 1) {
          const v = view.getBigUint64(o, true);
          o += 8;
          feed.feedUpdateTimestamp = bigintToSdkNumber(v);
        } else if (tag !== 0) {
          throw new RangeError('Invalid FeedUpdateTimestamp option tag');
        }
        break;
      }
      default:
        throw new RangeError(`Unknown feed property id: ${propId}`);
    }
  }
  return { feed, next: o };
}

/**
 * Decode a PriceUpdate payload produced by {@link encodePriceUpdate}.
 */
export function decodePriceUpdate(payload: Uint8Array): PriceUpdate {
  const min = 4 + 8 + 1 + 1;
  if (payload.length < min) {
    throw new RangeError('Price update payload too short');
  }
  expectMagic(payload, 0, PRICE_UPDATE_MAGIC, 'price update');
  const view = new DataView(
    payload.buffer,
    payload.byteOffset,
    payload.byteLength,
  );
  let o = 4;
  const timestampUs = view.getBigUint64(o, true);
  o += 8;
  const channelId = payload[o];
  o += 1;
  const feedsLen = payload[o];
  o += 1;
  const priceFeeds: ParsedFeedPayload[] = [];
  for (let f = 0; f < feedsLen; f++) {
    const decoded = decodeFeedPayload(payload, view, o);
    priceFeeds.push(decoded.feed);
    o = decoded.next;
  }
  if (o !== payload.length) {
    throw new RangeError('Trailing bytes in price update payload');
  }
  return {
    timestampUs: timestampUs.toString(),
    channelId,
    priceFeeds,
  };
}

const PYTH_MESSAGE_HEADER_LEN = 4 + 64 + 32 + 2;

/**
 * Decode a Solana-format Pyth message produced by {@link encodePythMessage}.
 */
export function decodePythMessage(message: Uint8Array): PythMessageParts {
  if (message.length < PYTH_MESSAGE_HEADER_LEN) {
    throw new RangeError('Pyth message too short');
  }
  expectMagic(message, 0, SOLANA_FORMAT_MAGIC, 'Solana format');
  const view = new DataView(
    message.buffer,
    message.byteOffset,
    message.byteLength,
  );
  const payloadLen = view.getUint16(100, true);
  const total = PYTH_MESSAGE_HEADER_LEN + payloadLen;
  if (message.length !== total) {
    throw new RangeError(
      `Pyth message length mismatch: expected ${total} bytes, got ${message.length}`,
    );
  }
  return {
    signature: message.subarray(4, 68).slice(),
    publicKey: message.subarray(68, 100).slice(),
    payload: message.subarray(102, total).slice(),
  };
}
