// Packages
import chalk from 'chalk'

// Ours
import toHost from './to-host'
import resolve4 from './dns'
import isZeitWorld from './is-zeit-world'
import {DOMAIN_VERIFICATION_ERROR} from './errors'
import Now from './'

const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/

export default class Alias extends Now {

  async ls(deployment) {
    if (deployment) {
      const target = await this.findDeployment(deployment)
      if (!target) {
        const err = new Error(`Aliases not found by "${deployment}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`)
        err.userError = true
        throw err
      }

      return this.listAliases(target.uid)
    }

    return this.listAliases()
  }

  async rm(_alias) {
    return this.retry(async bail => {
      const res = await this._fetch(`/now/aliases/${_alias.uid}`, {
        method: 'DELETE'
      })

      if (res.status === 403) {
        return bail(new Error('Unauthorized'))
      }

      if (res.status !== 200) {
        const err = new Error('Deletion failed. Try again later.')
        throw err
      }
    })
  }

  async findDeployment(deployment) {
    const list = await this.list()

    let key
    let val

    if (/\./.test(deployment)) {
      val = toHost(deployment)
      key = 'url'
    } else {
      val = deployment
      key = 'uid'
    }

    const depl = list.find(d => {
      if (d[key] === val) {
        if (this._debug) {
          console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`)
        }
        return true
      }

      // match prefix
      if (`${val}.now.sh` === d.url) {
        if (this._debug) {
          console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`)
        }
        return true
      }

      return false
    })

    return depl
  }

  async set(deployment, alias) {
    // make alias lowercase
    alias = alias.toLowerCase()

    // trim leading and trailing dots
    // for example: `google.com.` => `google.com`
    alias = alias
      .replace(/^\.+/, '')
      .replace(/\.+$/, '')

    const depl = await this.findDeployment(deployment)
    if (!depl) {
      const err = new Error(`Deployment not found by "${deployment}". Run ${chalk.dim('`now ls`')} to see your deployments.`)
      err.userError = true
      throw err
    }

    // evaluate the alias
    if (/\./.test(alias)) {
      alias = toHost(alias)
    } else {
      if (this._debug) {
        console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`)
      }

      alias = `${alias}.now.sh`
    }

    if (!domainRegex.test(alias)) {
      const err = new Error(`Invalid alias "${alias}"`)
      err.userError = true
      throw err
    }

    if (!/\.now\.sh$/.test(alias)) {
      console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`)
      console.log(`> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline('https://zeit.world')} for help)`)

      const {domain, nameservers} = await this.getNameservers(alias)

      if (this._debug) {
        console.log(`> [debug] Found domain ${domain} and nameservers ${nameservers}`)
      }

      try {
        await this.verifyOwnership(alias)
      } catch (err) {
        if (err.userError) {
          // a user error would imply that verification failed
          // in which case we attempt to correct the dns
          // configuration (if we can!)
          try {
            if (isZeitWorld(nameservers)) {
              console.log(`> Detected ${chalk.bold(chalk.underline('zeit.world'))} nameservers! Configuring records.`)
              const record = alias.substr(0, alias.length - domain.length)

              // lean up trailing and leading dots
              const _record = record
                .replace(/^\./, '')
                .replace(/\.$/, '')
              const _domain = domain
                .replace(/^\./, '')
                .replace(/\.$/, '')

              if (_record === '') {
                await this.setupRecord(_domain, '*')
              }

              await this.setupRecord(_domain, _record)

              this.recordSetup = true
              console.log('> DNS Configured! Verifying propagation…')

              try {
                await this.retry(() => this.verifyOwnership(alias), {retries: 10, maxTimeout: 8000})
              } catch (err2) {
                const e = new Error('> We configured the DNS settings for your alias, but we were unable to ' +
                            'verify that they\'ve propagated. Please try the alias again later.')
                e.userError = true
                throw e
              }
            } else {
              console.log(`> Resolved IP: ${err.ip ? `${chalk.underline(err.ip)} (unknown)` : chalk.dim('none')}`)
              console.log(`> Nameservers: ${nameservers && nameservers.length ? nameservers.map(ns => chalk.underline(ns)).join(', ') : chalk.dim('none')}`)
              throw err
            }
          } catch (e) {
            if (e.userError) {
              throw e
            }

            throw err
          }
        } else {
          throw err
        }
      }

      if (!isZeitWorld(nameservers)) {
        if (this._debug) {
          console.log(`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`)
        }

        await this.setupDomain(domain, {isExternal: true})
      }

      console.log(`> Verification ${chalk.bold('OK')}!`)
    }

    // unfortunately there's a situation where the verification
    // ownership code path in the `catch` above makes the
    // agent unexpectedly close. this is a workaround until
    // we figure out what's going on with `node-spdy`
    this._agent.close()
    this._agent._initAgent()

    const {created, uid} = await this.createAlias(depl, alias)
    if (created) {
      console.log(`${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}: ${chalk.bold(chalk.underline(`https://${alias}`))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}`)
    } else {
      console.log(`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`)
    }
  }

  createAlias(depl, alias) {
    return this.retry(async (bail, attempt) => {
      if (this._debug) {
        console.time(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`)
      }

      const res = await this._fetch(`/now/deployments/${depl.uid}/aliases`, {
        method: 'POST',
        body: {alias}
      })

      const body = await res.json()
      if (this._debug) {
        console.timeEnd(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`)
      }

      // 409 conflict is returned if it already exists
      if (res.status === 409) {
        return {uid: body.error.uid}
      }

      // no retry on authorization problems
      if (res.status === 403) {
        const code = body.error.code

        if (code === 'custom_domain_needs_upgrade') {
          const err = new Error(`Custom domains are only enabled for premium accounts. Please upgrade at ${chalk.underline('https://zeit.co/account')}.`)
          err.userError = true
          return bail(err)
        }

        if (code === 'alias_in_use') {
          const err = new Error(`The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`)
          err.userError = true
          return bail(err)
        }

        if (code === 'forbidden') {
          const err = new Error('The domain you are trying to use as an alias is already in use by a different account.')
          err.userError = true
          return bail(err)
        }

        return bail(new Error('Authorization error'))
      }

      // all other errors
      if (body.error) {
        const code = body.error.code

        if (code === 'deployment_not_found') {
          return bail(new Error('Deployment not found'))
        }

        if (code === 'cert_missing') {
          console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`)

          try {
            await this.createCert(alias)
          } catch (err) {
            // we bail to avoid retrying the whole process
            // of aliasing which would involve too many
            // retries on certificate provisioning
            return bail(err)
          }

          // try again, but now having provisioned the certificate
          return this.createAlias(depl, alias)
        }

        if (code === 'cert_expired') {
          console.log(`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`)

          try {
            await this.createCert(alias, {renew: true})
          } catch (err) {
            return bail(err)
          }
        }

        return bail(new Error(body.error.message))
      }

      // the two expected succesful cods are 200 and 304
      if (res.status !== 200 && res.status !== 304) {
        throw new Error('Unhandled error')
      }

      return body
    })
  }

  async setupRecord(domain, name) {
    await this.setupDomain(domain)

    if (this._debug) {
      console.log(`> [debug] Setting up record "${name}" for "${domain}"`)
    }

    const type = name === '' ? 'ALIAS' : 'CNAME'
    return this.retry(async (bail, attempt) => {
      if (this._debug) {
        console.time(`> [debug] /domains/${domain}/records #${attempt}`)
      }

      const res = await this._fetch(`/domains/${domain}/records`, {
        method: 'POST',
        body: {
          type,
          name: name === '' ? name : '*',
          value: 'alias.zeit.co'
        }
      })

      if (this._debug) {
        console.timeEnd(`> [debug] /domains/${domain}/records #${attempt}`)
      }

      if (res.status === 403) {
        return bail(new Error('Unauthorized'))
      }

      const body = await res.json()

      if (res.status !== 200) {
        throw new Error(body.error.message)
      }

      return body
    })
  }

  verifyOwnership(domain) {
    return this.retry(async bail => {
      const targets = await resolve4('alias.zeit.co')

      if (targets.length <= 0) {
        return bail(new Error('Unable to resolve alias.zeit.co'))
      }

      let ips = []

      try {
        ips = await resolve4(domain)
      } catch (err) {
        if (err.code === 'ENODATA' || err.code === 'ESERVFAIL' || err.code === 'ENOTFOUND') {
          // not errors per se, just absence of records
          if (this._debug) {
            console.log(`> [debug] No records found for "${domain}"`)
          }
        } else {
          throw err
        }
      }

      if (ips.length <= 0) {
        const err = new Error(DOMAIN_VERIFICATION_ERROR)
        err.userError = true
        return bail(err)
      }

      for (const ip of ips) {
        if (targets.indexOf(ip) === -1) {
          const err = new Error(`The domain ${domain} has an A record ${chalk.bold(ip)} that doesn't resolve to ${chalk.bold(chalk.underline('alias.zeit.co'))}.\n> ` + DOMAIN_VERIFICATION_ERROR)
          err.ip = ip
          err.userError = true
          return bail(err)
        }
      }
    })
  }
}