import { BufferReader, BufferWriter } from '@node-dlc/bufio';

import { ContractDescriptorType, MessageType } from '../MessageType';
import { IDlcMessage } from './DlcMessage';
import { PayoutFunction, PayoutFunctionV0JSON } from './PayoutFunction';
import { IRoundingIntervalsJSON, RoundingIntervals } from './RoundingIntervals';

export abstract class ContractDescriptor {
  public static deserialize(
    buf: Buffer,
  ): EnumeratedDescriptor | NumericalDescriptor {
    const reader = new BufferReader(buf);
    const typeId = Number(reader.readBigSize());

    switch (typeId) {
      case ContractDescriptorType.Enumerated:
        return EnumeratedDescriptor.deserialize(buf);
      case ContractDescriptorType.NumericOutcome:
        return NumericalDescriptor.deserialize(buf);
      default:
        throw new Error(
          `Contract descriptor type must be Enumerated (0) or NumericOutcome (1), got ${typeId}`,
        );
    }
  }

  /**
   * Creates a ContractDescriptor from JSON data (e.g., from test vectors)
   * @param json JSON object representing a contract descriptor
   */
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
  public static fromJSON(json: any): ContractDescriptor {
    if (!json) {
      throw new Error('contractDescriptor is required');
    }

    // Handle enumerated contract descriptor
    if (
      json.enumeratedContractDescriptor ||
      json.enumerated_contract_descriptor
    ) {
      return EnumeratedDescriptor.fromJSON(
        json.enumeratedContractDescriptor ||
          json.enumerated_contract_descriptor,
      );
    }
    // Handle numeric outcome contract descriptor
    else if (
      json.numericOutcomeContractDescriptor ||
      json.numeric_outcome_contract_descriptor
    ) {
      return NumericalDescriptor.fromJSON(
        json.numericOutcomeContractDescriptor ||
          json.numeric_outcome_contract_descriptor,
      );
    } else {
      throw new Error(
        'contractDescriptor must have either enumeratedContractDescriptor or numericOutcomeContractDescriptor',
      );
    }
  }

  public abstract contractDescriptorType: ContractDescriptorType;
  public abstract type: number; // For backward compatibility
  public abstract toJSON(): EnumeratedDescriptorJSON | NumericalDescriptorJSON;
  public abstract serialize(): Buffer;
}

/**
 * EnumeratedContractDescriptor contains information about a contract's outcomes
 * and their corresponding payouts (for enumerated outcomes).
 * This corresponds to the previous ContractDescriptorV0.
 */
export class EnumeratedDescriptor
  extends ContractDescriptor
  implements IDlcMessage
{
  public static contractDescriptorType = ContractDescriptorType.Enumerated;

  /**
   * Creates an EnumeratedContractDescriptor from JSON data
   * @param json JSON object representing an enumerated contract descriptor
   */
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
  public static fromJSON(json: any): EnumeratedDescriptor {
    const instance = new EnumeratedDescriptor();

    const payouts = json.payouts || [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    instance.outcomes = payouts.map((payout: any) => ({
      outcome: payout.outcome,
      localPayout: BigInt(payout.offerPayout || 0), // Use canonical offerPayout field
    }));

    return instance;
  }

  /**
   * Deserializes an enumerated_contract_descriptor message
   * @param buf
   */
  public static deserialize(buf: Buffer): EnumeratedDescriptor {
    const instance = new EnumeratedDescriptor();
    const reader = new BufferReader(buf);

    reader.readBigSize(); // read type (0)
    const numOutcomes = Number(reader.readBigSize());

    for (let i = 0; i < numOutcomes; i++) {
      const outcomeLen = Number(reader.readBigSize());
      const outcome = reader.readBytes(outcomeLen).toString();
      const localPayout = reader.readUInt64BE();

      instance.outcomes.push({
        outcome,
        localPayout,
      });
    }

    return instance;
  }

  /**
   * The type for enumerated_contract_descriptor message - using MessageType for IDlcMessage compatibility
   */
  public type = MessageType.ContractDescriptorV0; // For IDlcMessage compatibility

  /**
   * The contract descriptor type for new format
   */
  public contractDescriptorType = ContractDescriptorType.Enumerated;

  public outcomes: IOutcome[] = [];

  /**
   * Converts enumerated_contract_descriptor to JSON
   */
  public toJSON(): EnumeratedDescriptorJSON {
    // Return enum variant format for Rust compatibility
    return {
      enumeratedContractDescriptor: {
        payouts: this.outcomes.map((outcome) => ({
          outcome: outcome.outcome,
          offerPayout: Number(outcome.localPayout), // Use offerPayout to match Rust
        })),
      },
    };
  }

  /**
   * Serializes the enumerated_contract_descriptor message into a Buffer
   */
  public serialize(): Buffer {
    const writer = new BufferWriter();

    writer.writeBigSize(this.contractDescriptorType);
    writer.writeBigSize(this.outcomes.length);

    for (const outcome of this.outcomes) {
      writer.writeBigSize(outcome.outcome.length);
      writer.writeBytes(Buffer.from(outcome.outcome));
      writer.writeUInt64BE(outcome.localPayout);
    }

    return writer.toBuffer();
  }
}

/**
 * NumericOutcomeContractDescriptor contains information about a contract's outcomes
 * and their corresponding payouts (for numeric outcomes).
 * This corresponds to the previous ContractDescriptorV1.
 */
export class NumericalDescriptor
  extends ContractDescriptor
  implements IDlcMessage
{
  public static contractDescriptorType = ContractDescriptorType.NumericOutcome;

  /**
   * Creates a NumericOutcomeContractDescriptor from JSON data
   * @param json JSON object representing a numeric outcome contract descriptor
   */
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
  public static fromJSON(json: any): NumericalDescriptor {
    const instance = new NumericalDescriptor();

    instance.numDigits = json.numDigits || json.num_digits || 0;

    // Parse payout function using proper fromJSON method
    instance.payoutFunction = PayoutFunction.fromJSON(
      json.payoutFunction || json.payout_function,
    );

    // Parse rounding intervals using proper fromJSON method
    instance.roundingIntervals = RoundingIntervals.fromJSON(
      json.roundingIntervals || json.rounding_intervals,
    );

    return instance;
  }

  /**
   * Deserializes a numeric_outcome_contract_descriptor message
   * @param buf
   */
  public static deserialize(buf: Buffer): NumericalDescriptor {
    const instance = new NumericalDescriptor();
    const reader = new BufferReader(buf);

    reader.readBigSize(); // read type (1)
    instance.numDigits = reader.readUInt16BE();

    // Parse payout function - need to calculate its size to avoid consuming all bytes
    const payoutFunctionStartPos = reader.position;
    const tempPayoutFunction = PayoutFunction.deserialize(
      Buffer.from(reader.buffer.subarray(reader.position)),
    );
    instance.payoutFunction = tempPayoutFunction;

    // Skip past the payout function bytes
    const payoutFunctionSize = tempPayoutFunction.serialize().length;
    reader.position = payoutFunctionStartPos + payoutFunctionSize;

    // Parse remaining bytes as rounding intervals
    instance.roundingIntervals = RoundingIntervals.deserialize(
      Buffer.from(reader.buffer.subarray(reader.position)),
    );

    return instance;
  }

  /**
   * The type for numeric_outcome_contract_descriptor message - using MessageType for IDlcMessage compatibility
   */
  public type = MessageType.ContractDescriptorV1; // For IDlcMessage compatibility

  /**
   * The contract descriptor type for new format
   */
  public contractDescriptorType = ContractDescriptorType.NumericOutcome;

  public numDigits: number;

  public payoutFunction: PayoutFunction;

  public roundingIntervals: RoundingIntervals;

  /**
   * Validates correctness of all fields in the message
   * https://github.com/discreetlogcontracts/dlcspecs/blob/master/Messaging.md#the-contract_descriptor-type
   * @throws Will throw an error if validation fails
   */
  public validate(): void {
    this.roundingIntervals.validate();
  }

  /**
   * Converts numeric_outcome_contract_descriptor to JSON
   */
  public toJSON(): NumericalDescriptorJSON {
    // Return enum variant format for Rust compatibility
    return {
      numericOutcomeContractDescriptor: {
        numDigits: this.numDigits,
        payoutFunction: this.payoutFunction.toJSON(),
        roundingIntervals: this.roundingIntervals.toJSON(),
      },
    };
  }

  /**
   * Serializes the numeric_outcome_contract_descriptor message into a Buffer
   */
  public serialize(): Buffer {
    const writer = new BufferWriter();

    writer.writeBigSize(this.contractDescriptorType);
    writer.writeUInt16BE(this.numDigits);
    writer.writeBytes(this.payoutFunction.serialize());
    writer.writeBytes(this.roundingIntervals.serialize());

    return writer.toBuffer();
  }
}

// Legacy support - keeping old class names as aliases
export const ContractDescriptorV0 = EnumeratedDescriptor;
export const ContractDescriptorV1 = NumericalDescriptor;
export type ContractDescriptorV0 = EnumeratedDescriptor;
export type ContractDescriptorV1 = NumericalDescriptor;

interface IOutcome {
  outcome: string;
  localPayout: bigint;
}

// Rust-dlc enum variant format for EnumeratedDescriptor
export interface EnumeratedDescriptorJSON {
  enumeratedContractDescriptor: {
    payouts: Array<{
      outcome: string;
      offerPayout: number;
    }>;
  };
}

// Rust-dlc enum variant format for NumericalDescriptor
export interface NumericalDescriptorJSON {
  numericOutcomeContractDescriptor: {
    numDigits: number;
    payoutFunction: PayoutFunctionV0JSON;
    roundingIntervals: IRoundingIntervalsJSON;
  };
}

// Legacy interfaces for backward compatibility
export type ContractDescriptorV0JSON = EnumeratedDescriptorJSON;
export type ContractDescriptorV1JSON = NumericalDescriptorJSON;
