#!/usr/bin/env bash
#
# (c) 2017-2018 Konstantin Gredeskoul
#
# MIT License, distributed as part of `sym` ruby gem.
# • https://github.com/kigster/sym
#
#==============================================================================
#
# The purpose of this script is to transparently edit application secrets in a
# Rails apps or other repos. It simplifies the process of key import, as well
# as the direct editing.
#
# If you set some or (ideally) ALL variables below to values specific to your
# system, things will get easy.
#
# SYMIT__FOLDER is a relative folder to your project root, under which you
# might keep ALL of your encrypted files.  Alternatively, if you keep encrypted
# files sprinkled around your project, just leave it out, because it defaults
# to "." — the current folder, and search anything beneath.
#
# Variables:
#
#     # only search ./config folder
#     export SYMIT__FOLDER="config"
#
#     # this will be the name of your key in OS-X KeyChain
#     export SYMIT__KEY="MY_KEYCHAIN_NAME"
#
#     # This is the extension given to the encrypted files. Ideally, leave it
#     # be as ".enc"
#     export SYMIT__EXTENSION=".enc"
#
# And then
#
#     symit import key [ insecure ]           # import a key and password-protect it (or not)
#     symit auto application.yml.enc          # auto-decrypts it
#     symit auto application.yml              # auto-encrypts it
#     symit decrypt application.yml           # finds application.yml.enc and decrypts that.
#
#
# ...and vola! You are editing the encrypted file with sym from the root of
# your Rails application. Neat, no?
#

(   [[ -n $ZSH_EVAL_CONTEXT && $ZSH_EVAL_CONTEXT =~ :file$ ]] || \
    [[ -n $BASH_VERSION && $0 != "$BASH_SOURCE" ]]) && _s_=1 || _s_=0

(( $_s_ )) && _is_sourced=1
(( $_s_ )) || _is_sourced=0

function __lib::color::setup()  {
  if [[ -z "${setup_colors_loaded}" ]]; then

    export txtblk='\e[0;30m' # Black - Regular
    export txtred='\e[0;31m' # Red
    export txtgrn='\e[0;32m' # Green
    export txtylw='\e[0;33m' # Yellow
    export txtblu='\e[0;34m' # Blue
    export txtpur='\e[0;35m' # Purple
    export txtcyn='\e[0;36m' # Cyan
    export txtwht='\e[0;37m' # White

    export bldblk='\e[1;30m' # Black - Bold
    export bldred='\e[1;31m' # Red
    export bldgrn='\e[1;32m' # Green
    export bldylw='\e[1;33m' # Yellow
    export bldblu='\e[1;34m' # Blue
    export bldpur='\e[1;35m' # Purple
    export bldcyn='\e[1;36m' # Cyan
    export bldwht='\e[1;37m' # White

    export unkblk='\e[4;30m' # Black - Underline
    export undred='\e[4;31m' # Red
    export undgrn='\e[4;32m' # Green
    export undylw='\e[4;33m' # Yellow
    export undblu='\e[4;34m' # Blue
    export undpur='\e[4;35m' # Purple
    export undcyn='\e[4;36m' # Cyan
    export undwht='\e[4;37m' # White

    export bakblk='\e[40m' # Black - Background
    export bakred='\e[41m' # Red
    export bakgrn='\e[42m' # Green
    export bakylw='\e[43m' # Yellow
    export bakblu='\e[44m' # Blue
    export bakpur='\e[45m' # Purple
    export bakcyn='\e[46m' # Cyan
    export bakwht='\e[47m' # White

    export clr='\e[0m' # Text Reset
    export txtrst='\e[0m' # Text Reset
    export rst='\e[0m' # Text Reset
    export GREP_COLOR=32
    export setup_colors_loaded=1
  else
    [[ -n ${DEBUG} ]] && echo "colors already loaded..."
  fi
}

((${setup_colors_loaded})) ||__lib::color::setup

(( $_s_ )) || {
  printf "${bldred}This script is meant to be sourced into your environment,\n"
  printf "not run on a command line.${clr} \n\n"

  printf "Please add 'source $0' to your BASH initialization file,\n"
  printf "or run the following command:\n\n"

  printf "    \$ ${bldgrn}sym -B ~/.bash_profile${clr}\n\n"
  
  printf "${bldblu}Thanks for using Sym!${clr}\n"
  exit 1
}

function __lib::color::hr()  {
  local cols=${1:-${COLUMNS}}
  local char=${2:-"—"}
  local color=${3:-${txtylw}}

  printf "${color}"
  eval "printf \"%0.s${char}\" {1..${cols}}"
  printf "${clr}\n"
}

function __lib::color::h1()  {
  local title=$(echo "$*" | tr 'a-z' 'A-Z')
  len=${#title}
  printf "${bldylw}${title}\n"
  __lib::color::hr ${len} '—'
}

function __lib::color::h2()  {
  printf "${bldpur}$*${clr}\n"
}

function __lib::color::cursor_to_col()  {
  position=$1
  echo -en "\e[${position}C"
}

function __lib::color::cursor_to_row()  {
  position=$1
  echo -en "\e[${position}H"
}

function __symit::init()  {
  export SYMIT__EXTENSION=${SYMIT__EXTENSION:-'.enc'}
  export SYMIT__FOLDER=${SYMIT__FOLDER:-'.'}
  export SYMIT__KEY=${SYMIT__KEY}
}
function __symit::usage()  {
  __lib::color::setup
  __lib::color::h1 "symit"
  printf "
  This a BASH wrapper for the encryption tool (ruby gem) 'Sym'. It streamlines
  editing encrypted of files, importing and securing your key, and other
  actions.  The wrapper can be configured with ENV variables, or CLI flags.\n"

  printf "
  The easiest way to take advantage of this wrapper is to set the following
  environment variables, which removes the need to pass these values via the
  flags. These variables default to the shown values if not set elsewhere:${txtylw}

  export SYMIT__EXTENSION='${SYMIT__EXTENSION}'
  export SYMIT__FOLDER='${SYMIT__FOLDER}'
  export SYMIT__KEY='${SYMIT__KEY}'
  ${clr}\n"

  __lib::color::h2 "Usage:"
  printf "    ${bldgrn}symit [ action ] [ partial-file-path ] [ flags ]${clr}\n\n"

  __lib::color::h2 "Actions:"
  printf "    Action is the first word that defaults to ${bldylw}edit${clr}.\n\n"
  printf "    Valid actions are:\n"
  printf "    ${bldylw}— install       ${bldblk}ensures you are on the latest gem version\n"
  printf "    ${bldylw}— generate      ${bldblk}create a new secure key, and copies it to \n"
  printf "                    clipboard (if supported), otherwise prints to STDOUT\n"
  printf "                    Key is required, and used as a name within OSX KeyChain\n\n"
  printf "    ${bldylw}— import [key] [insecure]\n"
  printf "                    ${bldblk}imports the key from clipboard and adds password\n"
  printf "                    encryption unless 'insecure' is passed in\n\n"
  printf "    ${bldylw}— edit          ${bldblk}Finds all files, and opens them in $EDITOR\n"
  printf "    ${bldylw}— encrypt       ${bldblk}Encrypts files matching file-path\n"
  printf "    ${bldylw}— decrypt       ${bldblk}Adds the extension to file pattern and decrypts\n"
  printf "    ${bldylw}— auto          ${bldblk}encrypts decrypted file, and vice versa\n"

  echo
  __lib::color::h2 "Flags:"
  printf "    -f | --folder    DIR   ${bldblk}Top level folder to search.${clr}\n"
  printf "    -k | --key       KEY   ${bldblk}Key identifier${clr}\n"
  printf "    -x | --extension EXT   ${bldblk}Default extension of encrypted files.${clr}\n"
  printf "    -n | --dry-run         ${bldblk}Print stuff, but don't do it${clr}\n"
  printf "    -h | --help            ${bldblk}Show this help message${clr}\n"

  echo
  __lib::color::h2 'Encryption key identifier can be:'
  printf "${clr}\
  1. name of the keychain item storing the keychain (secure)
  2. name of the environment variable storing the Key (*)
  3. name of the file storing the key (*)
  4. the key itself (*)

  ${bldred}(*) 2-4 are insecure UNLESS the key is encrypted with a password.${clr}

  Please refer to README about generating password protected keys:
  ${bldblu}${undblu}https://github.com/kigster/sym#generating-the-key--examples${clr}\n\n"

  echo

  __lib::color::h1 'Examples:'

  printf "  Ex1: To import a key securely,\n"
  printf "     \$ ${bldgrn}symit${bldblu} import key ${clr}\n\n"

  printf "  Ex2.: To encrypt (or decrypt) ALL files in the 'config' directory:${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} encrypt|decrypt -a -f config ${clr}\n\n"

  printf "  Ex3: To decrypt all *.yml.enc files in the 'config' directory:${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} decrypt '*.yml' -f config ${clr}\n\n"

  printf "  Ex4: To edit an encrypted file ${txtblu}config/application.yml.enc${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} application.yml${clr}\n\n"

  printf "  Ex5.: To auto decrypt a file ${txtblu}config/settings/crypt/pass.yml.enc${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n"

  printf "  Ex6.: To automatically decide to either encrypt or decrypt a file,\n"
  printf "       based on the file extension. First example encrypts the file, second\n"
  printf "       decrypts it (because file extension is .enc):${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n"

  printf "  Ex7.: To encrypt a file ${txtblu}config/settings.yml${clr}\n"
  printf "     \$ ${bldgrn}symit${bldblu} encrypt config/settings.yml${clr}\n\n"

}
function __datum()   {
  date +"%m/%d/%Y.%H:%M:%S"
}

function __err()  {
  #__lib::color::cursor_to_col 0
  printf "${txtpur}[$(__datum)]  ${bldred}ERROR: ${txterr}$* ${bldylw}\n"
}

function __inf()  {
  #__lib::color::cursor_to_col 0
  printf "${txtpur}[$(__datum)]  ${bldgrn}INFO:  ${clr}${bldblu}$*${clr}\n"
}

function __symit::install::gem()  {
  __inf "Verifying current Sym version, please wait..."
  if [[ -z "${_symit__installed}" ]]; then
    current_version=$(gem list | grep sym | awk '{print $2}' | sed 's/(//g;s/)//g')
    if [[ -z "${current_version}" ]]; then
      gem install sym
    else
      local help=$(sym -h 2>&1)
      unset SYM_ARGS
      remote_version=$(gem search sym | egrep '^sym \(' | awk '{print $2}' | sed 's/(//g;s/)//g')
      if [[ "${remote_version}" != "${current_version}" ]]; then
        __inf "detected an older ${bldgrn}sym (${current_version})"
        __inf "installing ${bldgrn}sym (${remote_version})${clr}..."
        echo y | gem uninstall sym -a 2> /dev/null
        gem install sym
        export _symit__installed="yes"
        __inf "Installed sym version ${bldylw}$(sym --version)"
      else
        __inf "${bldgrn}sym${clr} ${txtblu}is on the latest version ${remote_version} already\n"
      fi
    fi
  fi
}

function __symit::files()  {
  eval $(__symit::files::cmd)
}

function __symit::files::cmd() {
  if [[ -n ${cli__opts[file]} && -n ${cli__opts[extension]} ]]; then
    local folder
    if [[ ${cli__opts[file]} =~ '/' ]]; then
      folder="${cli__opts[folder]}/$(dirname ${cli__opts[file]})"
    else
      folder="${cli__opts[folder]}"
    fi
    local file="$(basename ${cli__opts[file]})"
    local ext="${cli__opts[extension]}"

    if [[ "${cli__opts[action]}" == "auto" || "${cli__opts[action]}" == "encrypt" ]] ; then
      #find ${folder} -name "${file}" >&2
      printf "find ${folder} -name '${file}' -and -not -name '*${ext}'"
    else
      #find ${folder} -name "${file}${ext}" >&2
      printf "find ${folder} -name '${file}${ext}'"
    fi
  fi
}

function __symit::command()  {
  file=${1}
  if [[ -n "${cli__opts[key]}" && -n "${cli__opts[extension]}" ]]; then
    action="${cli__opts[action]}"
    flags="${sym__actions[${action}]}"
    if [[ ${action} =~ "key" ]]; then
      [[ -n ${cli__opts[verbose]} ]] && printf "processing key import action ${bldylw}${action}${clr}\n" >&2
      printf "sym ${flags} ${cli__opts[key]} "
    elif [[ ${action} =~ "generate" ]] ; then
      [[ -n ${cli__opts[verbose]} ]] && printf "processing generate key action ${bldylw}${action}${clr}\n" >&2
      if [[ -n $(which pbcopy) ]]; then
        out_key=/tmp/outkey
        command="sym ${flags} ${cli__opts[key]} -q -o ${out_key}; cat ${out_key} | pbcopy; rm -f ${out_key}"
        printf "${command}"
      else
        printf "sym ${flags} ${cli__opts[key]} "
      fi
    elif [[ -n ${file} ]] ; then
      ext="${cli__opts[extension]}"
      [[ -z ${ext} ]] && ext='.enc'
      ext=$(echo ${ext} | sed -E 's/[\*\/,.]//g')
      if [[ ${action} =~ "encrypt" ]]; then
        printf "sym ${flags} ${file} -ck ${cli__opts[key]} -o ${file}.${ext}"
      elif [[ ${action} =~ "decrypt" ]]; then
        new_name=$(echo ${file} | sed "s/\.${ext}//g")
        [[ "${new_name}" == "${file}" ]] && name="${file}.decrypted"
        printf "sym ${flags} ${file} -ck ${cli__opts[key]} -o ${new_name}"
      else
        printf "sym ${flags} ${file} -ck ${cli__opts[key]} "
      fi
    else
      printf "printf \"ERROR: not sure how to generate a correct command\\n\""
    fi
  fi
}

function __symit::cleanup()  {
  unset sym__actions
  unset cli__opts
}

function __symit::exit()  {
  code=${1:-0}
  __symit::cleanup
  echo -n ${code}
}

function __symit::print_cli_args()  {
  local -A args=$@
  __inf "action     ${bldylw}: ${cli__opts[action]}${clr}"
  __inf "key        ${bldylw}: ${cli__opts[key]}${clr}"
  __inf "file       ${bldylw}: ${cli__opts[file]}${clr}"
  __inf "extension  ${bldylw}: ${cli__opts[extension]}${clr}"
  __inf "folder     ${bldylw}: ${cli__opts[folder]}${clr}"
  __inf "verbose    ${bldylw}: ${cli__opts[verbose]}${clr}"
  __inf "dry_run    ${bldylw}: ${cli__opts[dry_run]}${clr}"
}

function __symit::args::needs_file()  {
  if [[ "${cli__opts[action]}" == 'edit' || \
  "${cli__opts[action]}" == 'auto' || \
  "${cli__opts[action]}" == 'encrypt' || \
  "${cli__opts[action]}" == 'decrypt' ]]; then
    printf 'yes'
  fi
}

function __symit::validate_args()  {
  if [[ -n $(__symit::args::needs_file) && -z ${cli__opts[file]} ]]; then
    __err "missing file argument, config/application.yml"
    return $(__symit::exit 2)
  fi

  if [[ -z "${cli__opts[key]}" ]]; then
    __err "Key was not defined, pass it with ${bldblu}-k KEY_ID${bldred}"
    __err "or set it via ${bldgrn}\$SYMIT__KEY${bldred} variable."
    return $(__symit::exit 4)
  fi

  if [[ -z ${cli__opts[extension]} ]]; then
    cli__opts[extension]='.enc'
  fi
}

function __symit::run()  {
  __symit::cleanup
  __symit::init

  declare -A cli__opts=(
    [verbose]=''
    [key]=${SYMIT__KEY}
    [extension]=${SYMIT__EXTENSION}
    [folder]=${SYMIT__FOLDER}
    [dry_run]=''
    [action]=edit
    [file]=''
  )

  declare -A sym__actions=(
    [generate]=' -cpgx '
    [edit]=' -t '
    [encrypt]='-e -f '
    [decrypt]='-d -f '
    [auto]=' -n '
    [key_secure]=' -iqcpx '
    [key_insecure]=' -iqcx '
  )

  if [[ -z $1 ]]; then
    __symit::usage
    return $(__symit::exit 0)
  fi

  while :; do
    case $1 in
      -h|-\?|--help)
          shift
          __symit::usage
          __symit::cleanup
          return $(__symit::exit 0)
          ;;

      -k|--key)
          shift
          if [[ -z $1 ]]; then
            __err "-k/--key requires an argument" && return $(__symit::exit 1)
          else
            cli__opts[key]=$1
            shift
          fi
          ;;

      -x|--extension)
          shift
          if [[ -z $1 ]]; then
            __err "-x/--extension requires an argument" && return $(__symit::exit 1)
          else
            cli__opts[extension]=${1}
            shift
          fi
          ;;

      -f|--folder)
          shift
          if [[ -z $1 ]]; then
            __err "-f/--folder requires an argument" && return $(__symit::exit 1)
          else
            cli__opts[folder]=${1}
            shift
          fi
          ;;

      -a|--all-files)
          shift
          cli__opts[file]="'*'"
          ;;

      -n|--dry-run)
          shift
          cli__opts[dry_run]="yes"
          ;;

      -v|--verbose)
          shift
          cli__opts[verbose]="yes"
          ;;

      import|key)
          shift
          cli__opts[action]="key_secure"
          ;;

      insecure)
          shift
          if [[ "${cli__opts[action]}" == 'key_secure' ]] ; then
            cli__opts[action]="key_insecure"
          fi
          ;;

      --) # End of all options.
          shift
          break
          ;;

      -?*)
          __err 'WARN: Unknown option: %s\n' "$1" >&2
          return $(__symit::exit 127)
          shift
          ;;


      ?*)
          param=$1
          if [[ -n "${sym__actions[${param}]}" ]]; then
            cli__opts[action]=${param}
          else
            cli__opts[file]=${1}
          fi
          shift
          ;;

      *) # Default case: If no more options then break out of the loop.
          break
          shift
    esac
  done

  [[ -n ${cli__opts[verbose]} ]] &&__symit::print_cli_args

  if [[ "${cli__opts[action]}" == 'install' ]]; then
    if [[ -n ${cli__opts[dry_run]} ]]; then
      __inf "This command verifies that Sym is properly installed,"
      __inf "and if not found — installs it."
      return $(__symit::exit 0)
    else
      __symit::install::gem
      return $(__symit::exit 0)
    fi
  fi

  __symit::validate_args

  changed_count=0

  if [[ -n "${cli__opts[dry_run]}" ]] ; then
    __lib::color::h1 "Dry Run — printing commands that would be run:"
    for file in $(__symit::files); do
      printf "   \$ ${bldblu}$(__symit::command ${file})${clr}\n"
    done
  else
    if [[ -n "${cli__opts[file]}" ]]; then
      [[ -n ${cli__opts[verbose]} ]] && __inf $(__symit::files)
      declare -a file_list
      for file in $(__symit::files); do
        file_list=(${file} "${file_list[*]}")
        __inf "❯ ${bldblu}$(__symit::command ${file})${clr}"
        eval $(__symit::command ${file})
        code=$?; [[ ${code} != 0 ]] && __err "sym returned non-zero code ${code}"
      done
      if [[ ${#file_list} == 0 ]]; then
        __inf "No files matched your specification. The following 'find' command"
        __inf "ran to find them: \n"
        __inf "   ${bldylw}$(__symit::files::cmd)${clr}\n\n"
        return $(__symit::exit 5)
      fi
    else
      [[ -n ${cli__opts[verbose]} ]] && __inf $(__symit::command)
      eval $(__symit::command)
      code=$?; [[ ${code} != 0 ]] && return $(__symits::exit ${code})
    fi
  fi
}

function symit()  {
  __symit::run $@
}