require 'hiera/backend/eyaml/parser/token'
require 'hiera/backend/eyaml/utils'
require 'hiera/backend/eyaml/encryptor'
require 'hiera/backend/eyaml'
require 'base64'

class Hiera
  module Backend
    module Eyaml
      module Parser
        class EncToken < Token
          @@tokens_map = {}
          @@encrypt_unchanged = true
          attr_reader :format, :cipher, :encryptor, :indentation, :plain_text, :id

          def self.encrypted_value(format, encryption_scheme, cipher, match, indentation = '')
            decryptor = Encryptor.find encryption_scheme
            plain_text = decryptor.decrypt(decryptor.decode(cipher))
            EncToken.new(format, plain_text, decryptor, cipher, match, indentation)
          end

          def self.decrypted_value(format, plain_text, encryption_scheme, match, id, indentation = '')
            encryptor = Encryptor.find encryption_scheme
            cipher = encryptor.encode(encryptor.encrypt(plain_text))
            id_number = id.nil? ? nil : id.gsub(/\(|\)/, '').to_i
            EncToken.new(format, plain_text, encryptor, cipher, match, indentation, id_number)
          end

          def self.plain_text_value(format, plain_text, encryption_scheme, match, id, indentation = '')
            encryptor = Encryptor.find encryption_scheme
            id_number = id.gsub(/\(|\)/, '').to_i unless id.nil?
            EncToken.new(format, plain_text, encryptor, '', match, indentation, id_number)
          end

          def self.tokens_map
            @@tokens_map
          end

          def self.set_encrypt_unchanged(encrypt_unchanged)
            @@encrypt_unchanged = encrypt_unchanged
          end

          def self.encrypt_unchanged
            @@encrypt_unchanged
          end

          def initialize(format, plain_text, encryptor, cipher, match = '', indentation = '', id = nil)
            @format = format
            @plain_text = Utils.convert_to_utf_8(plain_text)
            @encryptor = encryptor
            @cipher = cipher
            @indentation = indentation
            @id = id
            super(match)
          end

          def to_encrypted(args = {})
            label = args[:label]
            label_string = label.nil? ? '' : "#{label}: "
            format = args[:format].nil? ? @format : args[:format]
            encryption_method = args[:change_encryption]
            unless encryption_method.nil?
              @encryptor = Encryptor.find encryption_method
              @cipher = Base64.strict_encode64(@encryptor.encrypt(@plain_text))
            end
            case format
            when :block
              @cipher = @cipher.gsub(/\s/, '')
              chevron = args[:use_chevron].nil? || args[:use_chevron] ? ">\n" : ''
              "#{label_string}#{chevron}" + @indentation + "ENC[#{@encryptor.tag},#{@cipher}]".scan(/.{1,60}/).join("\n" + @indentation)
            when :string
              ciphertext = @cipher.gsub(/[\n\r]/, '')
              "#{label_string}ENC[#{@encryptor.tag},#{ciphertext}]"
            else
              raise "#{@format} is not a valid format"
            end
          end

          def to_decrypted(args = {})
            label = args[:label]
            label_string = label.nil? ? '' : "#{label}: "
            format = args[:format].nil? ? @format : args[:format]
            index = args[:index].nil? ? '' : "(#{args[:index]})"
            EncToken.tokens_map[index] = @plain_text if @@encrypt_unchanged == false

            case format
            when :block
              chevron = args[:use_chevron].nil? || args[:use_chevron] ? ">\n" : ''
              "#{label_string}#{chevron}" + indentation + "DEC#{index}::#{@encryptor.tag}[" + @plain_text + ']!'
            when :string
              "#{label_string}DEC#{index}::#{@encryptor.tag}[" + @plain_text + ']!'
            else
              raise "#{@format} is not a valid format"
            end
          end

          def to_plain_text
            @plain_text
          end
        end

        class EncTokenType < TokenType
          def create_enc_token(match, type, enc_comma, cipher, indentation = '')
            encryption_scheme = enc_comma.nil? ? Eyaml.default_encryption_scheme : enc_comma.split(',').first
            EncToken.encrypted_value(type, encryption_scheme, cipher, match, indentation)
          end
        end

        class EncHieraTokenType < EncTokenType
          def initialize
            @regex = %r{ENC\[(\w+,)?([a-zA-Z0-9+/ =\n]+?)\]}
            @string_token_type = EncStringTokenType.new
          end

          def create_token(string)
            @string_token_type.create_token(string.gsub(/\s/, ''))
          end
        end

        class EncStringTokenType < EncTokenType
          def initialize
            @regex = %r{ENC\[(\w+,)?([a-zA-Z0-9+/=]+?)\]}
          end

          def create_token(string)
            md = @regex.match(string)
            create_enc_token(string, :string, md[1], md[2])
          end
        end

        class EncBlockTokenType < EncTokenType
          def initialize
            @regex = %r{>\n(\s*)ENC\[(\w+,)?([a-zA-Z0-9+/=\s]+?)\]}
          end

          def create_token(string)
            md = @regex.match(string)
            create_enc_token(string, :block, md[2], md[3], md[1])
          end
        end

        class DecStringTokenType < TokenType
          def initialize
            @regex = /DEC(\(\d+\))?::(\w+)\[(.+?)\]!/m
          end

          def create_token(string)
            md = @regex.match(string)
            if EncToken.encrypt_unchanged == false && !md[1].nil? && (md[3] == EncToken.tokens_map[md[1]])
              return EncToken.plain_text_value(:string, md[3], md[2], string, md[1])
            end

            EncToken.decrypted_value(:string, md[3], md[2], string, md[1])
          end
        end

        class DecBlockTokenType < TokenType
          def initialize
            @regex = />\n(\s*)DEC(\(\d+\))?::(\w+)\[(.+?)\]!/m
          end

          def create_token(string)
            md = @regex.match(string)
            if EncToken.encrypt_unchanged == false && !md[2].nil? && (md[4] == EncToken.tokens_map[md[2]])
              return EncToken.plain_text_value(:string, md[4], md[3], string, md[2])
            end

            EncToken.decrypted_value(:block, md[4], md[3], string, md[2], md[1])
          end
        end
      end
    end
  end
end
