import type {
  Feed,
  MarketSession,
  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';

export { PRICE_UPDATE_MAGIC, SOLANA_FORMAT_MAGIC } from './types.js';

function u8(b: number): Uint8Array {
  if (b < 0 || b > 255) throw new RangeError('u8 out of range');
  return new Uint8Array([b & 0xff]);
}

function u16Le(n: number): Uint8Array {
  if (n < 0 || n > 0xffff) throw new RangeError('u16 out of range');
  const buf = new Uint8Array(2);
  new DataView(buf.buffer).setUint16(0, n, true);
  return buf;
}

function u32Le(n: number): Uint8Array {
  if (n < 0 || n > 0xffff_ffff) throw new RangeError('u32 out of range');
  const buf = new Uint8Array(4);
  new DataView(buf.buffer).setUint32(0, n, true);
  return buf;
}

function u64Le(n: bigint): Uint8Array {
  const buf = new Uint8Array(8);
  const view = new DataView(buf.buffer);
  const lo = Number(n & 0xffff_ffff_ffff_ffffn);
  const hi = Number((n >> 32n) & 0xffff_ffffn);
  view.setUint32(0, lo & 0xffff_ffff, true);
  view.setUint32(4, hi, true);
  return buf;
}

function i16Le(n: number): Uint8Array {
  const buf = new Uint8Array(2);
  new DataView(buf.buffer).setInt16(0, n, true);
  return buf;
}

function i64Le(n: bigint): Uint8Array {
  const buf = new Uint8Array(8);
  const view = new DataView(buf.buffer);
  const lo = Number(n & 0xffff_ffff_ffff_ffffn);
  const hi = Number(Number(n >> 32n) & 0xffff_ffff);
  view.setUint32(0, lo & 0xffff_ffff, true);
  view.setUint32(4, hi, true);
  return buf;
}

function concat(...chunks: Uint8Array[]): Uint8Array {
  const total = chunks.reduce((s, c) => s + c.length, 0);
  const out = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) {
    out.set(c, off);
    off += c.length;
  }
  return out;
}

/** Encode a single feed property (property_id byte + value bytes). */
function encodeFeedProperty(id: number, value: Uint8Array): Uint8Array {
  return concat(u8(id), value);
}

function toFeed(parsed: ParsedFeedPayload): Feed {
  const toBigInt = (v: string | number | undefined): bigint | null =>
    v === undefined ? null : BigInt(v);

  const toBigIntOptional = (v: string | number | undefined): bigint | null =>
    v === undefined ? null : BigInt(v);

  return {
    feedId: parsed.priceFeedId,
    price: parsed.price === undefined ? null : BigInt(parsed.price),
    bestBidPrice: toBigIntOptional(parsed.bestBidPrice),
    bestAskPrice: toBigIntOptional(parsed.bestAskPrice),
    publisherCount: parsed.publisherCount ?? 0,
    exponent: parsed.exponent ?? 0,
    confidence: toBigInt(parsed.confidence),
    fundingRate: toBigInt(parsed.fundingRate),
    fundingTimestamp: toBigInt(parsed.fundingTimestamp),
    fundingRateInterval: toBigInt(parsed.fundingRateInterval),
    marketSession: (parsed.marketSession as MarketSession) ?? 'Regular',
    emaPrice: toBigIntOptional(parsed.emaPrice),
    emaConfidence: toBigInt(parsed.emaConfidence),
    feedUpdateTimestamp: toBigInt(parsed.feedUpdateTimestamp),
  };
}

/** Encode feed properties that are defined, in the order expected by the parser. */
function encodeFeed(feed: Feed): Uint8Array {
  const parts: Uint8Array[] = [];

  if (feed.price !== undefined) {
    const v = feed.price === null ? 0n : feed.price;
    parts.push(encodeFeedProperty(PROP_ID.Price, i64Le(v)));
  }
  if (feed.bestBidPrice !== undefined) {
    const v = feed.bestBidPrice === null ? 0n : feed.bestBidPrice;
    parts.push(encodeFeedProperty(PROP_ID.BestBidPrice, i64Le(v)));
  }
  if (feed.bestAskPrice !== undefined) {
    const v = feed.bestAskPrice === null ? 0n : feed.bestAskPrice;
    parts.push(encodeFeedProperty(PROP_ID.BestAskPrice, i64Le(v)));
  }
  if (feed.publisherCount !== undefined) {
    parts.push(
      encodeFeedProperty(PROP_ID.PublisherCount, u16Le(feed.publisherCount)),
    );
  }
  if (feed.exponent !== undefined) {
    parts.push(encodeFeedProperty(PROP_ID.Exponent, i16Le(feed.exponent)));
  }
  if (feed.confidence !== undefined) {
    const v = feed.confidence === null ? 0n : feed.confidence;
    parts.push(encodeFeedProperty(PROP_ID.Confidence, i64Le(v)));
  }
  if (feed.fundingRate !== undefined) {
    if (feed.fundingRate === null) {
      parts.push(encodeFeedProperty(PROP_ID.FundingRate, u8(0)));
    } else {
      parts.push(
        encodeFeedProperty(
          PROP_ID.FundingRate,
          concat(u8(1), i64Le(feed.fundingRate)),
        ),
      );
    }
  }
  if (feed.fundingTimestamp !== undefined) {
    if (feed.fundingTimestamp === null) {
      parts.push(encodeFeedProperty(PROP_ID.FundingTimestamp, u8(0)));
    } else {
      parts.push(
        encodeFeedProperty(
          PROP_ID.FundingTimestamp,
          concat(u8(1), u64Le(feed.fundingTimestamp)),
        ),
      );
    }
  }
  if (feed.fundingRateInterval !== undefined) {
    if (feed.fundingRateInterval === null) {
      parts.push(encodeFeedProperty(PROP_ID.FundingRateInterval, u8(0)));
    } else {
      parts.push(
        encodeFeedProperty(
          PROP_ID.FundingRateInterval,
          concat(u8(1), u64Le(feed.fundingRateInterval)),
        ),
      );
    }
  }
  if (feed.marketSession !== undefined) {
    const idx = MARKET_SESSION_ORDER.indexOf(feed.marketSession);
    if (idx < 0) throw new RangeError('Invalid marketSession');
    parts.push(encodeFeedProperty(PROP_ID.MarketSession, u16Le(idx)));
  }
  if (feed.emaPrice !== undefined) {
    const v = feed.emaPrice === null ? 0n : feed.emaPrice;
    parts.push(encodeFeedProperty(PROP_ID.EmaPrice, i64Le(v)));
  }
  if (feed.emaConfidence !== undefined) {
    const v = feed.emaConfidence === null ? 0n : feed.emaConfidence;
    parts.push(encodeFeedProperty(PROP_ID.EmaConfidence, i64Le(v)));
  }
  if (feed.feedUpdateTimestamp !== undefined) {
    if (feed.feedUpdateTimestamp === null) {
      parts.push(encodeFeedProperty(PROP_ID.FeedUpdateTimestamp, u8(0)));
    } else {
      parts.push(
        encodeFeedProperty(
          PROP_ID.FeedUpdateTimestamp,
          concat(u8(1), u64Le(feed.feedUpdateTimestamp)),
        ),
      );
    }
  }

  const properties = concat(...parts);
  if (parts.length > 255)
    throw new RangeError('Feed has more than 255 properties');
  return concat(
    u32Le(feed.feedId),
    u8(parts.length), // number of properties (parser.repeat count), not byte length
    properties,
  );
}

/**
 * Encode a PriceUpdate to the binary payload format consumed by the on-chain parser.
 * This is the payload that goes inside a PythMessage.
 */
export function encodePriceUpdate(update: PriceUpdate): Uint8Array {
  if (update.priceFeeds.length > 255) {
    throw new RangeError('At most 255 feeds allowed');
  }
  const feeds: Feed[] = update.priceFeeds.map(toFeed);
  const feedBytes = feeds.map(encodeFeed);
  return concat(
    PRICE_UPDATE_MAGIC,
    u64Le(BigInt(update.timestampUs)),
    u8(update.channelId),
    u8(feeds.length),
    ...feedBytes,
  );
}

/**
 * Encode a full Pyth message (Solana format): magic + signature + key + payload length + payload.
 * Use this when you already have a signed payload (e.g. from a Pyth API or after signing locally).
 */
export function encodePythMessage(parts: PythMessageParts): Uint8Array {
  if (parts.signature.length !== 64) {
    throw new RangeError('signature must be 64 bytes');
  }
  if (parts.publicKey.length !== 32) {
    throw new RangeError('publicKey must be 32 bytes');
  }
  if (parts.payload.length > 0xffff) {
    throw new RangeError('payload length exceeds u16 max');
  }
  return concat(
    SOLANA_FORMAT_MAGIC,
    parts.signature,
    parts.publicKey,
    u16Le(parts.payload.length),
    parts.payload,
  );
}

/**
 * Encode a PythMessage whose payload is a PriceUpdate.
 * Signs the payload with the given secret key (32-byte Ed25519 seed).
 * Returns the full message bytes (hex-friendly).
 */
export async function encodeSignedPythMessage(
  update: PriceUpdate,
  secretKey: Uint8Array,
): Promise<Uint8Array> {
  if (secretKey.length !== 32) {
    throw new RangeError('secretKey must be 32 bytes (Ed25519 seed)');
  }
  const noble = await import('@noble/ed25519');
  const payload = encodePriceUpdate(update);
  const publicKey = await noble.getPublicKeyAsync(secretKey);
  const signature = await noble.signAsync(payload, secretKey);
  return encodePythMessage({ signature, publicKey, payload });
}
