import { TSchema, Data } from '@evolution-sdk/evolution';
import { option as O, function as F } from 'fp-ts';
import { match, P } from 'ts-pattern';
import { DEFAULT_SCHEMA_OPTIONS } from '../../types/evolution-schema-options';

const StakingPosLockedAmtSchema = TSchema.Array(
  TSchema.Tuple([
    TSchema.Integer,
    TSchema.Struct({
      voteAmt: TSchema.Integer,
      votingEnd: TSchema.Integer,
    }),
  ]),
);

export type StakingPosLockedAmt = typeof StakingPosLockedAmtSchema.Type;

const RewardSnapshotSchema = TSchema.Struct({
  snapshotAda: TSchema.Integer,
});

const StakingPositionSchema = TSchema.Struct({
  owner: TSchema.ByteArray,
  lockedAmount: StakingPosLockedAmtSchema,
  positionSnapshot: RewardSnapshotSchema,
});
export type StakingPosition = typeof StakingPositionSchema.Type;

const StakingManagerSchema = TSchema.Struct({
  totalStake: TSchema.Integer,
  managerSnapshot: RewardSnapshotSchema,
});
export type StakingManager = typeof StakingManagerSchema.Type;

const StakingDatumSchema = TSchema.Union(
  StakingManagerSchema,
  StakingPositionSchema,
);
type StakingDatum = typeof StakingDatumSchema.Type;

const StakingRedeemerSchema = TSchema.Union(
  TSchema.Struct(
    {
      CreateStakingPosition: TSchema.Struct(
        { creatorPkh: TSchema.ByteArray },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
  TSchema.Literal('UpdateTotalStake', { flatInUnion: true }),
  TSchema.Literal('Distribute', { flatInUnion: true }),
  TSchema.Struct(
    {
      AdjustStakedAmount: TSchema.Struct(
        { adjustAmount: TSchema.Integer },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
  TSchema.Literal('Unstake', { flatInUnion: true }),
  TSchema.Literal('Lock', { flatInUnion: true }),
  TSchema.Literal('UpgradeVersion', { flatInUnion: true }),
);

export type StakingRedeemer = typeof StakingRedeemerSchema.Type;

export function serialiseStakingRedeemer(r: StakingRedeemer): string {
  return Data.withSchema(
    StakingRedeemerSchema,
    DEFAULT_SCHEMA_OPTIONS,
  ).toCBORHex(r);
}

export function parseStakingRedeemer(datum: string): O.Option<StakingRedeemer> {
  try {
    return O.some(
      Data.withSchema(
        StakingRedeemerSchema,
        DEFAULT_SCHEMA_OPTIONS,
      ).fromCBORHex(datum),
    );
  } catch (_) {
    return O.none;
  }
}

export function parseStakingRedeemerOrThrow(datum: string): StakingRedeemer {
  return F.pipe(
    parseStakingRedeemer(datum),
    O.match(() => {
      throw new Error('Expected a StakingRedeemer datum.');
    }, F.identity),
  );
}

export function parseStakingPosition(datum: string): O.Option<StakingPosition> {
  try {
    return match(
      Data.withSchema(StakingDatumSchema, DEFAULT_SCHEMA_OPTIONS).fromCBORHex(
        datum,
      ),
    )
      .with({ owner: P.any }, (res) => O.some(res))
      .otherwise(() => O.none);
  } catch (_) {
    return O.none;
  }
}

export function parseStakingPositionOrThrow(datum: string): StakingPosition {
  return F.pipe(
    parseStakingPosition(datum),
    O.match(() => {
      throw new Error('Expected a StakingPosition datum.');
    }, F.identity),
  );
}

export function parseStakingManagerDatum(datum: string): StakingManager {
  return match(
    Data.withSchema(StakingDatumSchema, DEFAULT_SCHEMA_OPTIONS).fromCBORHex(
      datum,
    ),
  )
    .with({ totalStake: P.any }, (res) => res)
    .otherwise(() => {
      throw new Error('Expected a StakingPosition datum.');
    });
}

export function serialiseStakingDatum(d: StakingDatum): string {
  return Data.withSchema(StakingDatumSchema, DEFAULT_SCHEMA_OPTIONS).toCBORHex(
    d,
  );
}
