import { field, logger } from "@coder/logger"
import * as http from "http"
import * as https from "https"
import * as semver from "semver"
import * as url from "url"
import { version } from "./constants"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"

export interface Update {
  checked: number
  version: string
}

export interface LatestResponse {
  name: string
}

/**
 * Provide update information.
 */
export class UpdateProvider {
  private update?: Promise<Update>
  private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.

  public constructor(
    /**
     * The URL for getting the latest version of code-server. Should return JSON
     * that fulfills `LatestResponse`.
     */
    private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
    /**
     * Update information will be stored here. If not provided, the global
     * settings will be used.
     */
    private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
  ) {}

  /**
   * Query for and return the latest update.
   */
  public async getUpdate(force?: boolean): Promise<Update> {
    // Don't run multiple requests at a time.
    if (!this.update) {
      this.update = this._getUpdate(force)
      this.update.then(() => (this.update = undefined))
    }

    return this.update
  }

  private async _getUpdate(force?: boolean): Promise<Update> {
    const now = Date.now()
    try {
      let { update } = !force ? await this.settings.read() : { update: undefined }
      if (!update || update.checked + this.updateInterval < now) {
        const buffer = await this.request(this.latestUrl)
        const data = JSON.parse(buffer.toString()) as LatestResponse
        update = { checked: now, version: data.name.replace(/^v/, "") }
        await this.settings.write({ update })
      }
      logger.debug("got latest version", field("latest", update.version))
      return update
    } catch (error) {
      logger.error("Failed to get latest version", field("error", error.message))
      return {
        checked: now,
        version: "unknown",
      }
    }
  }

  /**
   * Return true if the currently installed version is the latest.
   */
  public isLatestVersion(latest: Update): boolean {
    logger.debug("comparing versions", field("current", version), field("latest", latest.version))
    try {
      return semver.lte(latest.version, version)
    } catch (error) {
      return true
    }
  }

  private async request(uri: string): Promise<Buffer> {
    const response = await this.requestResponse(uri)
    return new Promise((resolve, reject) => {
      const chunks: Buffer[] = []
      let bufferLength = 0
      response.on("data", (chunk) => {
        bufferLength += chunk.length
        chunks.push(chunk)
      })
      response.on("error", reject)
      response.on("end", () => {
        resolve(Buffer.concat(chunks, bufferLength))
      })
    })
  }

  private async requestResponse(uri: string): Promise<http.IncomingMessage> {
    let redirects = 0
    const maxRedirects = 10
    return new Promise((resolve, reject) => {
      const request = (uri: string): void => {
        logger.debug("Making request", field("uri", uri))
        const httpx = uri.startsWith("https") ? https : http
        const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
          if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
            response.destroy()
            return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
          }

          if (response.statusCode >= 300) {
            response.destroy()
            ++redirects
            if (redirects > maxRedirects) {
              return reject(new Error("reached max redirects"))
            }
            if (!response.headers.location) {
              return reject(new Error("received redirect with no location header"))
            }
            return request(url.resolve(uri, response.headers.location))
          }

          resolve(response)
        })
        client.on("error", reject)
      }
      request(uri)
    })
  }
}
