module Dpl
  module Providers
    class Releases < Provider
      status :beta

      full_name 'GitHub Releases'

      description sq(<<-str)
        tbd
      str

      gem 'octokit', '~> 4.14.0'
      gem 'mime-types', '~> 3.2.2'
      gem 'public_suffix', '~> 3.0.3'

      env :github, :releases

      required :token, [:username, :password]

      opt '--token TOKEN', 'GitHub oauth token (needs public_repo or repo permission)', secret: true, alias: :api_key
      opt '--username LOGIN', 'GitHub login name', alias: :user
      opt '--password PASS', 'GitHub password', secret: true
      opt '--repo SLUG', 'GitHub repo slug', default: :repo_slug
      opt '--file GLOB', 'File or glob to release to GitHub', default: '*', type: :array
      opt '--file_glob', 'Interpret files as globs', default: true
      opt '--overwrite', 'Overwrite files with the same name'
      opt '--prerelease', 'Identify the release as a prerelease'
      opt '--release_number NUM', 'Release number (overide automatic release detection)'
      opt '--release_notes STR', 'Content for the release notes', alias: :body
      opt '--release_notes_file PATH', 'Path to a file containing the release notes', note: 'will be ignored if --release_notes is given'
      opt '--draft', 'Identify the release as a draft'
      opt '--tag_name TAG', 'Git tag from which to create the release'
      opt '--target_commitish STR', 'Commitish value that determines where the Git tag is created from'
      opt '--name NAME', 'Name for the release'
      # should this have --url, like Pages does?

      needs :git

      msgs deploy:               'Deploying to repo: %{slug}',
           local_tag:            'Current tag is: %{local_tag}',
           login:                'Authenticated as %s',
           insufficient_scopes:  'Dpl does not have permission to upload assets. Make sure your token has the repo or public_repo scope.',
           insufficient_perm:    'Release resource not found. Make sure your token belongs to an account which has push permission to this repo.',
           overwrite_existing:   'File %s already exists, overwriting.',
           skip_existing:        'File %s already exists, skipping.',
           set_tag_name:         'Setting tag_name to %s',
           set_target_commitish: 'Setting target_commitish to %s',
           missing_file:         'File %s does not exist.',
           not_a_file:           '%s is not a file, skipping.'

      cmds git_fetch_tags:       'git fetch --tags'

      URL = 'https://api.github.com/repos/%s/releases/%s'

      OCTOKIT_OPTS = %i(
        repo
        name
        body
        prerelease
        release_number
        tag_name
        target_commitish
      )

      TIMEOUTS = {
        timeout: 180,
        open_timeout: 180
      }

      def validate
        info :deploy
        shell :git_fetch_tags if env_tag.nil?
        info :local_tag
      end

      def login
        user.login
        info :login, user.login
        error :insufficient_scopes unless sufficient_scopes?
      end

      def deploy
        upload_files
        api.update_release(url, octokit_opts)
      end

      def upload_files
        files.each { |file| upload_file(file) }
      end

      def upload_file(file)
        asset = asset(file)
        return info :skip_existing, file if asset && !overwrite?
        delete(asset, file) if asset
        api.upload_asset(url, file, name: File.basename(file), content_type: content_type(file))
      end

      def delete(asset, file)
        info :overwrite_existing, file
        api.delete_release_asset(asset.url)
      end

      def octokit_opts
        opts = with_tag(self.opts.dup)
        opts = with_target_commitish(opts)
        opts = opts.select { |key, _| OCTOKIT_OPTS.include?(key) }
        compact(opts.merge(body: release_notes, draft: draft?))
      end
      memoize :octokit_opts

      def with_tag(opts)
        return opts if tag_name? || draft?
        info :set_tag_name, local_tag
        opts.merge(tag_name: local_tag)
      end

      def with_target_commitish(opts)
        return opts if target_commitish? || !same_repo?
        info :set_target_commitish, git_sha
        opts.merge(target_commitish: git_sha)
      end

      def content_type(file)
        type = MIME::Types.type_for(file).first
        type ||= 'application/octet-stream'
        type.to_s
      end

      def url
        if release_number?
          URL % [slug, release_number]
        elsif release
          release.rels[:self].href
        else
          create_release.rels[:self].href
        end
      end
      memoize :url

      def release
        releases.detect { |release| release.tag_name == local_tag }
      end

      def create_release
        api.create_release(slug, local_tag, octokit_opts.merge(draft: true))
      rescue Octokit::NotFound
        error :insufficient_perm
      end

      def local_tag
        env_tag || git_tag
      end

      def env_tag
        tag = ENV['TRAVIS_TAG']
        tag unless tag.to_s.empty?
      end

      def sufficient_scopes?
        api.scopes.include?('public_repo') || api.scopes.include?('repo')
      end

      def slug
        repo || repo_slug
      end

      def same_repo?
        slug == repo_slug
      end

      def asset(path)
        api.release_assets(url).detect { |asset| asset.name == path }
      end

      def release_notes
        super || release_notes_file || nil
      end

      def release_notes_file
        release_notes_file? && exists?(super) && read(super)
      end

      def user
        @user ||= api.user
      end

      def releases
        @releases ||= api.releases(slug)
      end

      def api
        @api ||= Octokit::Client.new(**creds, auto_paginate: true, connection_options: { request: TIMEOUTS })
      end

      def creds
        username && password ? { login: username, password: password } : { access_token: token }
      end

      def files
        files = file_glob? ? Dir.glob("{#{file.join(',')}}").uniq : file
        files = files.select { |file| exists?(file) }
        files.select { |file| file?(file) }
      end

      def exists?(file)
        return true if File.exists?(file)
        error :missing_file, file
        false
      end

      def file?(file)
        return true if File.file?(file)
        warn :not_a_file, file
        false
      end
    end
  end
end