import { encode as hexEncode, decode as hexDecode } from '@stablelib/hex'
import { equal } from '@stablelib/constant-time'
import { decode as base64decode } from '@stablelib/base64'
import unfetch from 'isomorphic-unfetch'

import { CommitTransactionStellar, VerificationTransaction } from './types'

import { z } from 'zod'
const HorizonTransactionResponse = z
  .object({
    memo: z.string().regex(/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{3}=|[A-Za-z\d+/]{2}==)?$/), // Base64 https://regexland.com/base64/
    memo_type: z.string().refine((v) => v === 'hash', { message: 'Transaction failed : memo_type field is not hash' }),
    ledger: z.number(),
    successful: z.boolean().refine((v) => v === true, { message: 'Transaction failed : success field is not true' }),
  })
type HorizonTransactionResponse = z.infer<typeof HorizonTransactionResponse>

const HorizonLedgerResponse = z
  .object({
    closed_at: z.string(),
    sequence: z.number(),
  })
type HorizonLedgerResponse = z.infer<typeof HorizonLedgerResponse>

export async function verifyStellar(inputTx: CommitTransactionStellar, testing: boolean | undefined): Promise<VerificationTransaction> {
  const machineBaseUrl = testing ? 'https://horizon-testnet.stellar.org' : 'https://horizon.stellar.org'
  const machineUrl = `${machineBaseUrl}/transactions/${inputTx.hash}`

  const foundTxResp = await unfetch(machineUrl)

  if (!foundTxResp.ok) {
    throw new Error(`Stellar : failed to fetch transaction hash '${inputTx.hash}' : status ${foundTxResp.status} : ${foundTxResp.statusText}`)
  }

  // Extract key attributes from the transaction
  const foundTx: HorizonTransactionResponse = (await foundTxResp.json()) as HorizonTransactionResponse

  try {
    HorizonTransactionResponse.parse(foundTx)
  } catch (error) {
    if (error instanceof z.ZodError) {
      const joinedIssues: string = error.issues
        .map((issue: z.ZodIssue) => {
          return `${issue.code} : [${issue.path.join(', ')}] : ${issue.message}`
        })
        .join('; ')
      throw new Error(`Stellar : failed to parse transaction hash '${inputTx.hash}' : ${joinedIssues}`)
    } else if (error instanceof Error) {
      throw new Error(`Stellar : failed to parse transaction hash '${inputTx.hash}' : ${error.message}`)
    }
  }

  // Memo is returned as a base64 encoded string (not url safe string)
  const foundTxMemoDecoded = base64decode(foundTx.memo)

  // This is the most important check. The on-chain memo MUST match the inputHash Truestamp recorded.
  if (!equal(foundTxMemoDecoded, hexDecode(inputTx.inputHash))) {
    throw new Error(
      `Stellar : on-chain 'memo' did not match recorded 'memo'. Expected '${inputTx.inputHash}' but received '${hexEncode(
        foundTxMemoDecoded,
      ).toLowerCase()}' for transaction hash '${inputTx.hash}'`,
    )
  }

  // The found ledger must match the ledger Truestamp recorded
  if (foundTx.ledger !== inputTx.ledger) {
    throw new Error(`Stellar : on-chain transaction 'ledger' did not match recorded 'ledger' for transaction hash '${inputTx.hash}'`)
  }

  // Fetch the ledger associated with the transaction, since only the ledger contains the closed_at timestamp.
  const ledgerResp = await unfetch(`${machineBaseUrl}/ledgers/${inputTx.ledger}`)

  if (!ledgerResp.ok) {
    throw new Error(`Stellar : failed to fetch ledger '${inputTx.ledger}'`)
  }

  // Extract key attributes from the ledger, the closed_at timestamp
  const ledger: HorizonLedgerResponse = (await ledgerResp.json()) as HorizonLedgerResponse

  try {
    HorizonLedgerResponse.parse(ledger)
  } catch (error) {
    if (error instanceof z.ZodError) {
      const joinedIssues: string = error.issues
        .map((issue: z.ZodIssue) => {
          return `${issue.code} : [${issue.path.join(', ')}] : ${issue.message}`
        })
        .join('; ')
      throw new Error(`Stellar : failed to parse ledger '${inputTx.ledger}' : ${joinedIssues}`)
    } else if (error instanceof Error) {
      throw new Error(`Stellar : failed to parse ledger '${inputTx.ledger}' : ${error.message}`)
    }
  }

  // Construct a browser page URL with transaction details for human use
  const urlHumanBase = 'https://stellar.expert/explorer'
  const urlHumanEnv = testing ? 'testnet' : 'public'
  const urlHuman = `${urlHumanBase}/${urlHumanEnv}/tx/${inputTx.hash}`

  const vt: VerificationTransaction = {
    verified: true,
    intent: 'stellar',
    transaction: inputTx,
    timestamp: ledger.closed_at,
    urls: {
      human: [urlHuman],
      machine: [machineUrl]
    },
  }

  return VerificationTransaction.parse(vt)
}
