import type { CustomerStatement } from '@zync/types'
import { renderStatementHtml } from './statement-html'

const HTML_TO_PDF_URL = 'https://api.html-to-pdf.zync.is'
const PDF_HEADER = new TextEncoder().encode('%PDF-')
const PDF_EOF = new TextEncoder().encode('%%EOF')
const PDF_STARTXREF = new TextEncoder().encode('startxref')
const MIN_PDF_BYTES = 64
const PDF_HEADER_SEARCH_LIMIT = 1024
const PDF_EOF_SEARCH_LIMIT = 1024

export type StatementPdfErrorCode =
  | 'STATEMENT_PDF_UPSTREAM_ERROR'
  | 'STATEMENT_PDF_INVALID_CONTENT_TYPE'
  | 'STATEMENT_PDF_INVALID_BYTES'

export class StatementPdfError extends Error {
  readonly code: StatementPdfErrorCode
  readonly upstreamStatus?: number

  constructor(code: StatementPdfErrorCode, message: string, upstreamStatus?: number) {
    super(message)
    this.name = 'StatementPdfError'
    this.code = code
    this.upstreamStatus = upstreamStatus
  }
}

function containsBytes(bytes: Uint8Array, needle: Uint8Array, start: number, end: number): boolean {
  const limit = Math.min(end, bytes.length - needle.length + 1)
  for (let offset = Math.max(0, start); offset < limit; offset += 1) {
    let matches = true
    for (let index = 0; index < needle.length; index += 1) {
      if (bytes[offset + index] !== needle[index]) {
        matches = false
        break
      }
    }
    if (matches) return true
  }
  return false
}

function isPdfWhitespace(byte: number): boolean {
  return byte === 0x00 || byte === 0x09 || byte === 0x0a || byte === 0x0c || byte === 0x0d || byte === 0x20
}

export function validateStatementPdf(bytes: Uint8Array): void {
  if (
    bytes.byteLength < MIN_PDF_BYTES ||
    !containsBytes(bytes, PDF_HEADER, 0, PDF_HEADER_SEARCH_LIMIT) ||
    !containsBytes(bytes, PDF_STARTXREF, 0, bytes.length) ||
    !containsBytes(bytes, PDF_EOF, Math.max(0, bytes.length - PDF_EOF_SEARCH_LIMIT), bytes.length)
  ) {
    throw new StatementPdfError('STATEMENT_PDF_INVALID_BYTES', 'statement PDF provider returned invalid PDF bytes')
  }

  let eofOffset = bytes.length - PDF_EOF.length
  while (eofOffset >= 0 && !containsBytes(bytes, PDF_EOF, eofOffset, eofOffset + 1)) eofOffset -= 1
  if (eofOffset < 0) {
    throw new StatementPdfError('STATEMENT_PDF_INVALID_BYTES', 'statement PDF provider returned invalid PDF bytes')
  }

  for (let offset = eofOffset + PDF_EOF.length; offset < bytes.length; offset += 1) {
    if (!isPdfWhitespace(bytes[offset]!)) {
      throw new StatementPdfError('STATEMENT_PDF_INVALID_BYTES', 'statement PDF provider returned invalid PDF bytes')
    }
  }
}

export async function renderStatementPdf(
  statement: CustomerStatement,
  pdfFetch: typeof fetch = fetch,
): Promise<Uint8Array> {
  const response = await pdfFetch(HTML_TO_PDF_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
    body: renderStatementHtml(statement, 'he'),
  })
  if (!response.ok) {
    throw new StatementPdfError(
      'STATEMENT_PDF_UPSTREAM_ERROR',
      `statement PDF provider returned status ${response.status}`,
      response.status,
    )
  }

  const contentType = response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase()
  if (contentType !== 'application/pdf') {
    throw new StatementPdfError(
      'STATEMENT_PDF_INVALID_CONTENT_TYPE',
      'statement PDF provider returned an invalid content type',
      response.status,
    )
  }

  const bytes = new Uint8Array(await response.arrayBuffer())
  validateStatementPdf(bytes)
  return bytes
}
