import path from 'node:path'
import process from 'node:process'
import { execFile, findCommandFromPath } from '../utils/command'
import { PLUGIN_DATA_DIR } from '../utils/constant'
import {
  copyDir,
  ensureDirExist,
  exists,
  getHash,
  readDir,
  readFile
} from '../utils/fs'
import { debug_log, error_log, prettyLog, warn_log } from '../utils/logger'

import Config from './config'
import Downloader from './downloader'
import Record from './record'
import { type BaseSource, CodingSource, GithubSource } from './source'
import VersionManger from './version'

export type SourceType = 'github' | 'coding' | BaseSource

export type MkcertBaseOptions = {
  /**
   * Whether to force generate
   */
  force?: boolean

  /**
   * Automatically upgrade mkcert
   *
   * @default false
   */
  autoUpgrade?: boolean

  /**
   * Specify mkcert download source
   *
   * @default github
   */
  source?: SourceType

  /**
   * If your network is restricted, you can specify a local binary file instead of downloading, it should be an absolute path
   *
   * @default none
   */
  mkcertPath?: string

  /**
   * Proxy used when downloading mkcert binary.
   *
   * @example http://127.0.0.1:7890
   */
  proxy?: string

  /**
   * Whether to print download progress logs when fetching mkcert binary.
   *
   * @default true
   */
  downloadProgress?: boolean

  /**
   * The location to save the files, such as key and cert files
   */
  savePath?: string

  /**
   * The name of private key file generated by mkcert
   */
  keyFileName?: string

  /**
   * The name of cert file generated by mkcert
   */
  certFileName?: string
}

export type MkcertOptions = MkcertBaseOptions
class Mkcert {
  private force?: boolean
  private autoUpgrade?: boolean
  private sourceType: SourceType
  private savePath: string

  private source: BaseSource
  private localMkcert?: string
  private proxy?: string
  private downloadProgress: boolean
  private savedMkcert: string
  private keyFilePath: string
  private certFilePath: string

  private config: Config

  public static create(options: MkcertOptions) {
    return new Mkcert(options)
  }

  private constructor(options: MkcertOptions) {
    const {
      force,
      autoUpgrade,
      source,
      mkcertPath,
      proxy,
      downloadProgress = true,
      savePath = PLUGIN_DATA_DIR,
      keyFileName = 'dev.pem',
      certFileName = 'cert.pem'
    } = options

    this.force = force
    this.autoUpgrade = autoUpgrade
    this.localMkcert = mkcertPath
    this.proxy = proxy?.trim() || undefined
    this.downloadProgress = downloadProgress
    this.savePath = path.resolve(savePath)
    this.keyFilePath = path.resolve(this.savePath, keyFileName)
    this.certFilePath = path.resolve(this.savePath, certFileName)
    this.sourceType = source || 'github'

    if (this.sourceType === 'github') {
      this.source = GithubSource.create()
    } else if (this.sourceType === 'coding') {
      this.source = CodingSource.create()
    } else {
      this.source = this.sourceType
    }

    this.savedMkcert = path.resolve(
      this.savePath,
      process.platform === 'win32' ? 'mkcert.exe' : 'mkcert'
    )

    this.config = new Config({ savePath: this.savePath })
  }

  private async getMkcertBinary() {
    if (this.localMkcert) {
      if (await exists(this.localMkcert)) {
        return this.localMkcert
      } else {
        warn_log(
          `${this.localMkcert} does not exist, please check the mkcertPath parameter`
        )
      }
    }

    if (await exists(this.savedMkcert)) {
      return this.savedMkcert
    }

    return findCommandFromPath('mkcert')
  }

  private async checkCAExists() {
    const files = await readDir(this.savePath)
    return files.some(file => file.includes('rootCA'))
  }

  private async retainExistedCA() {
    if (await this.checkCAExists()) {
      return
    }

    const mkcertBinary = await this.getMkcertBinary()
    if (!mkcertBinary) {
      return
    }
    const commandArgs = ['-CAROOT']

    debug_log(`Exec ${mkcertBinary} ${commandArgs.join(' ')}`)

    const commandResult = await execFile(mkcertBinary, commandArgs)
    const caDirPath = path.resolve(commandResult.stdout.toString().trim())

    if (caDirPath === this.savePath) {
      return
    }

    const caDirExists = await exists(caDirPath)

    if (!caDirExists) {
      return
    }

    await copyDir(caDirPath, this.savePath)
  }

  private async getCertificate() {
    const key = await readFile(this.keyFilePath)
    const cert = await readFile(this.certFilePath)

    return {
      key,
      cert
    }
  }

  private async createCertificate(hosts: string[]) {
    const mkcertBinary = await this.getMkcertBinary()

    if (!mkcertBinary) {
      error_log(
        `Mkcert does not exist, unable to generate certificate for ${hosts.join(', ')}`
      )
      throw new Error('Mkcert binary is not found')
    }

    await ensureDirExist(this.savePath)
    await this.retainExistedCA()

    const commandArgs = [
      '-install',
      '-key-file',
      this.keyFilePath,
      '-cert-file',
      this.certFilePath,
      ...hosts
    ]

    await execFile(mkcertBinary, commandArgs, {
      env: {
        ...process.env,
        CAROOT: this.savePath,
        JAVA_HOME: undefined
      }
    })

    debug_log(
      `The list of generated files:\n${this.keyFilePath}\n${this.certFilePath}`
    )
  }

  private getLatestHash = async () => {
    return {
      key: await getHash(this.keyFilePath),
      cert: await getHash(this.certFilePath)
    }
  }

  private async regenerate(record: Record, hosts: string[]) {
    await this.createCertificate(hosts)

    const hash = await this.getLatestHash()

    record.update({ hosts, hash })
  }

  public async init() {
    await ensureDirExist(this.savePath)
    await this.config.init()

    const mkcertBinary = await this.getMkcertBinary()

    if (!mkcertBinary) {
      await this.initMkcert()
    } else if (this.autoUpgrade) {
      await this.upgradeMkcert()
    }
  }

  private async getSourceInfo() {
    const sourceInfo = await this.source.getSourceInfo()

    if (!sourceInfo) {
      const message =
        typeof this.sourceType === 'string'
          ? `Unsupported platform. Unable to find a binary file for ${process.platform
          } platform with ${process.arch} arch on ${this.sourceType === 'github'
            ? 'https://github.com/FiloSottile/mkcert/releases'
            : 'https://liuweigl.coding.net/p/github/artifacts?hash=8d4dd8949af543159c1b5ac71ff1ff72'
          }`
          : 'Please check your custom "source", it seems to return invalid result'
      throw new Error(message)
    }

    return sourceInfo
  }

  private async initMkcert() {
    const sourceInfo = await this.getSourceInfo()

    warn_log('The mkcert does not exist, download it now')

    await this.downloadMkcert(
      sourceInfo.downloadUrl,
      this.savedMkcert,
      this.proxy
    )
  }

  private async upgradeMkcert() {
    const versionManger = new VersionManger({ config: this.config })
    const sourceInfo = await this.getSourceInfo()

    if (!sourceInfo) {
      warn_log('Can not obtain download information of mkcert, update skipped')
      return
    }

    const versionInfo = versionManger.compare(sourceInfo.version)

    if (!versionInfo.shouldUpdate) {
      debug_log('Mkcert is kept latest version, update skipped')
      return
    }

    if (versionInfo.breakingChange) {
      debug_log(
        'The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped',
        versionInfo.currentVersion,
        versionInfo.nextVersion
      )
      return
    }

    debug_log(
      'The current version of mkcert is %s, and the latest version is %s, mkcert will be updated',
      versionInfo.currentVersion,
      versionInfo.nextVersion
    )

    await this.downloadMkcert(
      sourceInfo.downloadUrl,
      this.savedMkcert,
      this.proxy
    )
    versionManger.update(versionInfo.nextVersion)
  }

  private async downloadMkcert(
    sourceUrl: string,
    distPath: string,
    proxy?: string
  ) {
    const downloader = Downloader.create()
    await downloader.download({
      downloadUrl: sourceUrl,
      savedPath: distPath,
      proxy,
      logProgress: this.downloadProgress
    })
  }

  public async renew(hosts: string[]) {
    const record = new Record({ config: this.config })

    if (this.force) {
      debug_log('Certificate is forced to regenerate')

      await this.regenerate(record, hosts)
      return
    }

    if (!record.contains(hosts)) {
      debug_log(
        `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate`
      )

      await this.regenerate(record, hosts)
      return
    }

    const hash = await this.getLatestHash()

    if (!record.equal(hash)) {
      debug_log(
        `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog(
          hash
        )}, start regenerate certificate`
      )

      await this.regenerate(record, hosts)
      return
    }

    debug_log('Neither hosts nor hash has changed, skip regenerate certificate')
  }

  /**
   * Get certificates
   *
   * @param hosts host collection
   * @returns cretificates
   */
  public async install(hosts: string[]) {
    if (hosts.length) {
      await this.renew(hosts)
    }

    return await this.getCertificate()
  }
}

export default Mkcert
