import { Buffer } from 'buffer'
import {
  GenericFilehandle,
  FilehandleOptions,
  Stats,
  Fetcher,
  PolyfilledResponse,
} from './filehandle'
import { encode } from 'base-64'

export default class RemoteFile implements GenericFilehandle {
  protected url: string
  protected auth: any = {}
  private _stat?: Stats
  private fetchImplementation: Fetcher
  private baseOverrides: any = {}

  private async getBufferFromResponse(
    response: PolyfilledResponse,
  ): Promise<Buffer> {
    if (typeof response.buffer === 'function') {
      return response.buffer()
    } else if (typeof response.arrayBuffer === 'function') {
      const resp = await response.arrayBuffer()
      return Buffer.from(resp)
    } else {
      throw new TypeError(
        'invalid HTTP response object, has no buffer method, and no arrayBuffer method',
      )
    }
  }

  public constructor(source: string, opts: FilehandleOptions = {}) {
    this.url = source
    this.auth = opts.auth || {}
    const fetch = opts.fetch || globalThis.fetch.bind(globalThis)
    if (!fetch) {
      throw new TypeError(
        `no fetch function supplied, and none found in global environment`,
      )
    }
    if (opts.overrides) {
      this.baseOverrides = opts.overrides
    }
    this.fetchImplementation = fetch
  }

  public async fetch(
    input: RequestInfo,
    init: RequestInit | undefined,
  ): Promise<PolyfilledResponse> {
    let response
    try {
      init = init || {}
      if (this.auth && this.auth.user && this.auth.password) {
        init.credentials = 'include'
        init.headers = {}
        init.headers.Authorization = `Basic ${encode(
          this.auth.user + ':' + this.auth.password,
        )}`
      }
      response = await this.fetchImplementation(input, {
        ...init,
        // headers: headers,
      })
    } catch (e) {
      if (`${e}`.includes('Failed to fetch')) {
        // refetch to to help work around a chrome bug (discussed in
        // generic-filehandle issue #72) in which the chrome cache returns a
        // CORS error for content in its cache.  see also
        // https://github.com/GMOD/jbrowse-components/pull/1511
        console.warn(
          `generic-filehandle: refetching ${input} to attempt to work around chrome CORS header caching bug`,
        )
        init = init || {}
        if (this.auth && this.auth.user && this.auth.password) {
          init.credentials = 'include'
          init.headers = {}
          init.headers.Authorization = `Basic ${encode(
            this.auth.user + ':' + this.auth.password,
          )}`
        }
        response = await this.fetchImplementation(input, {
          ...init,
          // headers: headers,
          cache: 'reload',
        })
      } else {
        throw e
      }
    }
    return response
  }

  public async read(
    buffer: Buffer,
    offset = 0,
    length: number,
    position = 0,
    opts: FilehandleOptions = {},
  ): Promise<{ bytesRead: number; buffer: Buffer }> {
    const { headers = {}, signal, overrides = {} } = opts
    if (length < Infinity) {
      headers.range = `bytes=${position}-${position + length}`
    } else if (length === Infinity && position !== 0) {
      headers.range = `bytes=${position}-`
    }
    const args = {
      ...this.baseOverrides,
      ...overrides,
      headers: {
        ...headers,
        ...overrides.headers,
        ...this.baseOverrides.headers,
      },
      method: 'GET',
      redirect: 'follow',
      mode: 'cors',
      signal,
    }
    if (this.auth && this.auth.user && this.auth.password) {
      headers.Authorization = `Basic ${encode(
        this.auth.user + ':' + this.auth.password,
      )}`
      args.credentials = 'include'
    }
    const response = await this.fetch(this.url, args)

    if (!response.ok) {
      throw new Error(
        `HTTP ${response.status} ${response.statusText} ${this.url}`,
      )
    }

    if (
      (response.status === 200 && position === 0) ||
      response.status === 206
    ) {
      const responseData = await this.getBufferFromResponse(response)
      const bytesCopied = responseData.copy(
        buffer,
        offset,
        0,
        Math.min(length, responseData.length),
      )

      // try to parse out the size of the remote file
      const res = response.headers.get('content-range')
      const sizeMatch = /\/(\d+)$/.exec(res || '')
      if (sizeMatch && sizeMatch[1]) {
        this._stat = { size: parseInt(sizeMatch[1], 10) }
      }

      return { bytesRead: bytesCopied, buffer }
    }

    if (response.status === 200) {
      throw new Error('${this.url} fetch returned status 200, expected 206')
    }

    if (response.status === 404) {
      return { bytesRead: 0, buffer }
    }

    // TODO: try harder here to gather more information about what the problem is
    throw new Error(`HTTP ${response.status} fetching ${this.url}`)
  }

  public async readFile(): Promise<Buffer>
  public async readFile(options: BufferEncoding): Promise<string>
  public async readFile<T extends undefined>(
    options:
      | Omit<FilehandleOptions, 'encoding'>
      | (Omit<FilehandleOptions, 'encoding'> & { encoding: T }),
  ): Promise<Buffer>
  public async readFile<T extends BufferEncoding>(
    options: Omit<FilehandleOptions, 'encoding'> & { encoding: T },
  ): Promise<string>
  readFile<T extends BufferEncoding>(
    options: Omit<FilehandleOptions, 'encoding'> & { encoding: T },
  ): T extends BufferEncoding ? Promise<Buffer> : Promise<Buffer | string>
  public async readFile(
    options: FilehandleOptions | BufferEncoding = {},
  ): Promise<Buffer | string> {
    let encoding
    let opts
    if (typeof options === 'string') {
      encoding = options
      opts = {}
    } else {
      encoding = options.encoding
      opts = options
      delete opts.encoding
    }
    const { headers = {}, signal, overrides = {} } = opts
    const args = {
      headers,
      method: 'GET',
      redirect: 'follow',
      mode: 'cors',
      signal,
      ...this.baseOverrides,
      ...overrides,
    }
    if (this.auth && this.auth.user && this.auth.password) {
      headers.Authorization = `Basic ${encode(
        this.auth.user + ':' + this.auth.password,
      )}`
      args.credentials = 'include'
    }
    const response = await this.fetch(this.url, args)

    if (!response) {
      throw new Error('generic-filehandle failed to fetch')
    }

    if (response.status === 404) {
      return Buffer.alloc(1)
    }

    if (response.status !== 200) {
      throw Object.assign(
        new Error(`HTTP ${response.status} fetching ${this.url}`),
        {
          status: response.status,
        },
      )
    }
    if (encoding === 'utf8') {
      return response.text()
    }
    if (encoding) {
      throw new Error(`unsupported encoding: ${encoding}`)
    }
    return this.getBufferFromResponse(response)
  }

  public async stat(): Promise<Stats> {
    if (!this._stat) {
      const buf = Buffer.allocUnsafe(10)
      await this.read(buf, 0, 10, 0)
      if (!this._stat) {
        throw new Error(`unable to determine size of file at ${this.url}`)
      }
    }
    return this._stat
  }

  public async close(): Promise<void> {
    return
  }
}
