import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, concatBytes, utf8ToBytes } from '@stacks/common';

import { ClarityType, ClarityValue, serializeCV } from './clarity';
import { StacksMessageType } from './constants';
import { signMessageHashRsv, StacksPrivateKey } from './keys';

// Refer to SIP018 https://github.com/stacksgov/sips/
// > asciiToBytes('SIP018')
export const STRUCTURED_DATA_PREFIX = new Uint8Array([0x53, 0x49, 0x50, 0x30, 0x31, 0x38]);

export function hashStructuredData(structuredData: ClarityValue): Uint8Array {
  return sha256(serializeCV(structuredData));
}

const hash256BytesLength = 32;

function isDomain(value: ClarityValue): boolean {
  if (value.type !== ClarityType.Tuple) return false;
  // Check that the tuple has at least 'name', 'version' and 'chain-id'
  if (!['name', 'version', 'chain-id'].every(key => key in value.data)) return false;
  // Check each key is of the right type
  if (!['name', 'version'].every(key => value.data[key].type === ClarityType.StringASCII))
    return false;

  if (value.data['chain-id'].type !== ClarityType.UInt) return false;
  return true;
}

export function encodeStructuredData({
  message,
  domain,
}: {
  message: ClarityValue;
  domain: ClarityValue;
}): Uint8Array {
  const structuredDataHash: Uint8Array = hashStructuredData(message);
  if (!isDomain(domain)) {
    throw new Error(
      "domain parameter must be a valid domain of type TupleCV with keys 'name', 'version', 'chain-id' with respective types StringASCII, StringASCII, UInt"
    );
  }
  const domainHash: Uint8Array = hashStructuredData(domain);

  return concatBytes(STRUCTURED_DATA_PREFIX, domainHash, structuredDataHash);
}

export type DecodedStructuredData = {
  domainHash: Uint8Array;
  messageHash: Uint8Array;
};

export function decodeStructuredDataSignature(
  signature: string | Uint8Array
): DecodedStructuredData {
  const encodedMessageBytes: Uint8Array =
    typeof signature === 'string' ? utf8ToBytes(signature) : signature;
  const domainHash = encodedMessageBytes.slice(
    STRUCTURED_DATA_PREFIX.length,
    STRUCTURED_DATA_PREFIX.length + hash256BytesLength
  );
  const messageHash = encodedMessageBytes.slice(STRUCTURED_DATA_PREFIX.length + hash256BytesLength);
  return {
    domainHash,
    messageHash,
  };
}

export interface StructuredDataSignature {
  readonly type: StacksMessageType.StructuredDataSignature;
  data: string;
}

/**
 * Signs a structured message (ClarityValue) and a domain (ClarityValue) using a private key.
 * The resulting signature along with the original message can be verified using {@link verifyMessageSignature}
 * @returns A recoverable signature (in RSV order)
 */
export function signStructuredData({
  message,
  domain,
  privateKey,
}: {
  message: ClarityValue;
  domain: ClarityValue;
  privateKey: StacksPrivateKey;
}): StructuredDataSignature {
  const structuredDataHash: string = bytesToHex(sha256(encodeStructuredData({ message, domain })));

  const { data } = signMessageHashRsv({
    messageHash: structuredDataHash,
    privateKey,
  });
  return {
    data,
    type: StacksMessageType.StructuredDataSignature,
  };
}
