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

import { DEFAULT_SCHEMA_OPTIONS } from '../../types/evolution-schema-options';
import {
  AddressSchema,
  AssetClassSchema,
  OutputReferenceSchema,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  divideOnChainCompatible,
  zeroNegatives,
} from '../../utils/bigint-utils';

export const SPIntegerSchema = TSchema.Struct({
  value: TSchema.Integer,
});

export type SPInteger = typeof SPIntegerSchema.Type;

const AccountActionSchema = TSchema.Union(
  TSchema.Literal('Create', { flatInUnion: true }),
  TSchema.Struct(
    {
      Adjust: TSchema.Struct(
        {
          amount: TSchema.Integer,
          outputAddress: AddressSchema,
        },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
  TSchema.Struct(
    {
      Close: TSchema.Struct(
        { outputAddress: AddressSchema, maxTxFee: TSchema.Integer },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
);

export type AccountAction = typeof AccountActionSchema.Type;

const SumSnapshotSchema = TSchema.Struct({
  sumVal: SPIntegerSchema,
  isLastInEpoch: TSchema.Boolean,
  isFirstSnapshot: TSchema.Boolean,
});

export type SumSnapshot = typeof SumSnapshotSchema.Type;

const EpochToScaleKeySchema = TSchema.Struct({
  epoch: TSchema.Integer,
  scale: TSchema.Integer,
});

export type EpochToScaleKey = typeof EpochToScaleKeySchema.Type;

const EpochToScaleToSumEntrySchema = TSchema.Tuple([
  EpochToScaleKeySchema,
  SumSnapshotSchema,
]);

export type EpochToScaleToSumEntry = typeof EpochToScaleToSumEntrySchema.Type;

const StateSnapshotSchema = TSchema.Struct({
  productVal: SPIntegerSchema,
  depositVal: SPIntegerSchema,
  epoch: TSchema.Integer,
  scale: TSchema.Integer,
});
export type StateSnapshot = typeof StateSnapshotSchema.Type;

const AssetSnapshotSchema = TSchema.Struct({
  currentSumVal: SPIntegerSchema,
  epoch2scale2sum: TSchema.Array(EpochToScaleToSumEntrySchema),
});
export type AssetSnapshot = typeof AssetSnapshotSchema.Type;

const AssetStateSchema = TSchema.Tuple([AssetClassSchema, AssetSnapshotSchema]);
export type AssetState = typeof AssetStateSchema.Type;

export const StabilityPoolContentSchema = TSchema.Struct({
  iasset: TSchema.ByteArray,
  state: StateSnapshotSchema,
  assetStates: TSchema.Array(AssetStateSchema),
});

export type StabilityPoolContent = typeof StabilityPoolContentSchema.Type;

export const AccountContentSchema = TSchema.Struct({
  owner: TSchema.ByteArray,
  iasset: TSchema.ByteArray,
  state: StateSnapshotSchema,
  assetSums: TSchema.Array(TSchema.Tuple([AssetClassSchema, SPIntegerSchema])),
  request: TSchema.NullOr(AccountActionSchema),
  lastRequestProcessingTime: TSchema.Integer,
});

export type AccountContent = typeof AccountContentSchema.Type;

export const SnapshotEpochToScaleToSumContentSchema = TSchema.Struct({
  snapshot: TSchema.Array(EpochToScaleToSumEntrySchema),
  iasset: TSchema.ByteArray,
  collateralAsset: AssetClassSchema,
});

export type SnapshotEpochToScaleToSumContent =
  typeof SnapshotEpochToScaleToSumContentSchema.Type;

export const StabilityPoolDatumSchema = TSchema.Union(
  TSchema.Struct(
    { StabilityPool: StabilityPoolContentSchema },
    { flatInUnion: true },
  ),
  TSchema.Struct({ Account: AccountContentSchema }, { flatInUnion: true }),
  TSchema.Struct(
    { SnapshotEpochToScaleToSum: SnapshotEpochToScaleToSumContentSchema },
    { flatInUnion: true },
  ),
);

const E2S2SIndexSchema = TSchema.Union(
  TSchema.Struct(
    { StabilityPoolListIdx: TSchema.Integer },
    { flatInUnion: true },
  ),
  TSchema.Struct(
    {
      RefInputIdx: TSchema.Struct(
        {
          refInputIdx: TSchema.Integer,
          snapshotListIdx: TSchema.Integer,
        },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
);

export type E2S2SIndex = typeof E2S2SIndexSchema.Type;

const E2S2SIndicesPerAssetSchema = TSchema.Array(
  TSchema.Tuple([E2S2SIndexSchema, TSchema.NullOr(E2S2SIndexSchema)]),
);

export type E2S2SIndicesPerAsset = typeof E2S2SIndicesPerAssetSchema.Type;

const ProcessRequestAccountContentSchema = TSchema.Struct(
  {
    poolInputIdx: TSchema.Integer,
    accountInputIdx: TSchema.Integer,
    e2s2sIdxs: E2S2SIndicesPerAssetSchema,
    currentTime: TSchema.Integer,
  },
  { flatFields: true },
);

export type ProcessRequestAccountContent =
  typeof ProcessRequestAccountContentSchema.Type;

export const StabilityPoolRedeemerSchema = TSchema.Union(
  TSchema.Struct({ RequestAction: AccountActionSchema }, { flatInUnion: true }),
  TSchema.Struct(
    {
      ProcessRequestPool: TSchema.Struct(
        {
          poolInputIdx: TSchema.Integer,
          accountInputIdx: TSchema.Integer,
        },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
  TSchema.Struct(
    {
      ProcessRequestAccount: ProcessRequestAccountContentSchema,
    },
    { flatInUnion: true },
  ),
  TSchema.Literal('AnnulRequest', { flatInUnion: true }),
  TSchema.Struct(
    {
      LiquidateCDP: TSchema.Struct(
        {
          cdpIdx: TSchema.Integer,
        },
        { flatFields: true },
      ),
    },
    { flatInUnion: true },
  ),
  TSchema.Literal('RecordEpochToScaleToSum', { flatInUnion: true }),
  TSchema.Literal('UpgradeVersion', { flatInUnion: true }),
);

export type StabilityPoolRedeemer = typeof StabilityPoolRedeemerSchema.Type;

const ActionReturnDatumSchema = TSchema.Union(
  TSchema.Struct(
    {
      IndigoStabilityPoolAccountAdjustment: OutputReferenceSchema,
    },
    { flatInUnion: true },
  ),
  TSchema.Struct(
    {
      IndigoStabilityPoolAccountClosure: OutputReferenceSchema,
    },
    { flatInUnion: true },
  ),
);

export type ActionReturnDatum = typeof ActionReturnDatumSchema.Type;

export function serialiseActionReturnDatum(d: ActionReturnDatum): string {
  return Data.withSchema(
    ActionReturnDatumSchema,
    DEFAULT_SCHEMA_OPTIONS,
  ).toCBORHex(d);
}

export function serialiseStabilityPoolRedeemer(
  r: StabilityPoolRedeemer,
): string {
  return Data.withSchema(
    StabilityPoolRedeemerSchema,
    DEFAULT_SCHEMA_OPTIONS,
  ).toCBORHex(r);
}

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

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

export function serialiseStabilityPoolDatum(
  d: typeof StabilityPoolDatumSchema.Type,
  /**
   * This is necessary to change only in case of execute propose asset.
   */
  useIndefiniteMaps: boolean = false,
): string {
  return Data.withSchema(StabilityPoolDatumSchema, {
    ...DEFAULT_SCHEMA_OPTIONS,
    useIndefiniteMaps: useIndefiniteMaps,
  }).toCBORHex(d);
}

export function parseStabilityPoolDatum(
  datum: string,
): O.Option<StabilityPoolContent> {
  try {
    return match(
      Data.withSchema(
        StabilityPoolDatumSchema,
        DEFAULT_SCHEMA_OPTIONS,
      ).fromCBORHex(datum),
    )
      .with({ StabilityPool: P.select() }, (res) => O.some(res))
      .otherwise(() => O.none);
  } catch (_) {
    return O.none;
  }
}
export function parseStabilityPoolDatumOrThrow(
  datum: string,
): StabilityPoolContent {
  return F.pipe(
    parseStabilityPoolDatum(datum),
    O.match(() => {
      throw new Error('Expected stability pool datum.');
    }, F.identity),
  );
}

export function parseAccountDatum(datum: string): O.Option<AccountContent> {
  try {
    return match(
      Data.withSchema(
        StabilityPoolDatumSchema,
        DEFAULT_SCHEMA_OPTIONS,
      ).fromCBORHex(datum),
    )
      .with({ Account: P.select() }, (res) => O.some(res))
      .otherwise(() => O.none);
  } catch (_) {
    return O.none;
  }
}
export function parseAccountDatumOrThrow(datum: string): AccountContent {
  return F.pipe(
    parseAccountDatum(datum),
    O.match(() => {
      throw new Error('Expected account datum.');
    }, F.identity),
  );
}

export function parseSnapshotEpochToScaleToSumDatum(
  datum: string,
): O.Option<SnapshotEpochToScaleToSumContent> {
  try {
    return match(
      Data.withSchema(
        StabilityPoolDatumSchema,
        DEFAULT_SCHEMA_OPTIONS,
      ).fromCBORHex(datum),
    )
      .with({ SnapshotEpochToScaleToSum: P.select() }, (res) => O.some(res))
      .otherwise(() => O.none);
  } catch (_) {
    return O.none;
  }
}

export function parseSnapshotEpochToScaleToSumDatumOrThrow(
  datum: string,
): SnapshotEpochToScaleToSumContent {
  return F.pipe(
    parseSnapshotEpochToScaleToSumDatum(datum),
    O.match(() => {
      throw new Error('Expected snapshot e2s2s datum.');
    }, F.identity),
  );
}

/** SP Integer */
const spPrecision: bigint = 1_000_000_000_000_000_000n;

export function mkSPInteger(value: bigint): SPInteger {
  return { value: value * spPrecision };
}

export function fromSPInteger(value: SPInteger): bigint {
  return divideOnChainCompatible(value.value, spPrecision);
}

export function spAdd(a: SPInteger, b: SPInteger): SPInteger {
  return { value: a.value + b.value };
}

export function spSub(a: SPInteger, b: SPInteger): SPInteger {
  return { value: a.value - b.value };
}

export function spMul(a: SPInteger, b: SPInteger): SPInteger {
  return { value: divideOnChainCompatible(a.value * b.value, spPrecision) };
}

export function spDiv(a: SPInteger, b: SPInteger): SPInteger {
  return { value: divideOnChainCompatible(a.value * spPrecision, b.value) };
}

export function spZeroNegatives(a: SPInteger): SPInteger {
  return { value: zeroNegatives(a.value) };
}
