#!/usr/bin/env python3

# -*- coding: utf-8 -*-

# NAME

# inji - Render jina2 templates to stdout

from __future__ import print_function, with_statement

import argparse
import atexit
import fnmatch
from jinja2 import DebugUndefined, StrictUndefined, Undefined, make_logging_undefined
from jinja2 import Environment, FileSystemLoader
from jinja2.exceptions import TemplateNotFound, UndefinedError
import json
import logging
import os
from os.path import abspath, basename, dirname, exists, expandvars, isdir, isfile, join
import re
import shutil
import sys
import tempfile
import yaml

def render_template(
    template,
    in_vars,
    strict_mode_behaviour
  ):

  m = strict_mode_behaviour
  if   m in ['strict', 'StrictUndefined']:
    Handler = StrictUndefined
  elif m in ['empty',  'Undefined']:
    Handler = Undefined
  elif m in ['keep',   'DebugUndefined']:
    Handler = DebugUndefined

  # Setup debug logging on STDERR to have the jinja2 engine emit
  # its activities
  root = logging.getLogger(template)
  root.setLevel(logging.DEBUG)
  handler = logging.StreamHandler(sys.stderr)
  logformat = '%(name)s %(levelname)s: %(message)s'
  formatter = logging.Formatter(logformat)
  handler.setFormatter(formatter)
  root.addHandler(handler)

  UndefinedHandler = make_logging_undefined( logger=root, base=Handler )

  # This is contra the design philosophy of jinja where templates are part of
  # bigger projects and includes are possible relative to the target template.
  # But we are a tool that renders (simple) templates and while we do not
  # preclude complex use cases, we assume the target is the "master" template
  # and any includes, etc it uses are relative to where it resides (not the
  # current directory of the process).
  rootdir = dirname(template)

  j2_env = Environment( loader=FileSystemLoader(rootdir),
                        undefined=UndefinedHandler,
                        trim_blocks=True )

  try:
    template = basename(template)
    yield j2_env.get_template(template).render(in_vars)
  except UndefinedError as e:
    raise UndefinedError( "variable {} in template '{}'".format(
            str(e), template) ) from e


def read_in_vars(yaml_file):
  yaml_file = yaml_file.__str__()
  with open(yaml_file, 'r') as f:
    try:
      in_vars = yaml.load(f, Loader=yaml.SafeLoader)
      if in_vars is None:
        raise TypeError("'{}' contains no data".format(file))
    except TypeError as exc:
      raise exc
  return in_vars

def recursive_iglob(rootdir='.', pattern='*'):
  for root, dirnames, filenames in os.walk(rootdir):
    for filename in fnmatch.filter(filenames, pattern):
      yield os.path.join(root, filename)

def path(fspath, type='file'):
  """
  Checks if a filesystem path exists with the correct type
  """

  fspath = abspath(expandvars(str(fspath)))
  msg = None
  prefix = "path '{0}'".format(fspath)

  if not exists(fspath):
    msg = "{0} does not exist".format(prefix)

  if type == 'file' and isdir(fspath):
    msg = "{0} is not a file".format(prefix)

  if msg is not None:
    raise argparse.ArgumentTypeError(msg)

  return fspath

def file_or_stdin(file):
  # /dev/stdin is a special case allowing bash (and other shells?) to name stdin
  # as a file. While python should have no problem reading from it, we actually
  # read template relative to the template's basedir and /dev has no templates.
  if file == '-' or file == '/dev/stdin':
    return '-'
  return path(file)

def cli_args():
  parser = argparse.ArgumentParser(description='inji - jinja template renderer')
  required = parser.add_argument_group('required arguments')

  required.add_argument('-t', '-f', '--template',
    action = 'store',  required=False, type=file_or_stdin,
    dest='template', default='-',
    help='/path/to/template.j2 (defaults to -)'
  )

  parser.add_argument('-c', '--config', '-j', '--json',
    action = 'store', required=False,
    type=lambda x: json.loads(x) if len(x) > 2 else '',
    dest='config',
    help='-c "{var1:\"blah\", var2:\"blah\"}"'
  )

  parser.add_argument('-o', '--overlay', '--overlay-dir',
    action = 'append', required=False, type=lambda p, t='dir': path(p, t),
    dest='overlays', default=[],
    help='/path/to/overlay/'
  )

  parser.add_argument('-v', '-p', '--vars-file', '--vars',
    action = 'append', required=False, type=lambda p, t='file': path(p, t),
    dest='in_vars_files', default=[],
    help='/path/to/vars.yaml'
  )

  parser.add_argument('--strict-mode', '-s',
    action = 'store', required=False, type=str,
    dest='strict_mode', default='strict',
    choices=[ 'strict', 'empty', 'keep',
              'StrictUndefined', 'Undefined', 'DebugUndefined' ],
    help='Refer to http://jinja.pocoo.org/docs/2.10/api/#undefined-types'
  )

  return parser.parse_args()


if __name__ == '__main__':

  args = cli_args()

  # in_vars in the local configuration files - precendence 5
  config_files = fnmatch.filter(os.listdir('.'), "*inji.y*ml")

  # in_vars in the overlay directories - precendence 4
  args.overlays = list( f for d in args.overlays
                          for f in recursive_iglob(d, '*.y*ml') )

  # This will hold the final vars dict merged from various available sources
  in_vars = {}

  for file in [ *config_files,
                *args.overlays,
                *args.in_vars_files ]:
    in_vars.update(read_in_vars(file))

  # in_vars from environment variables - precedence 3
  in_vars.update(os.environ)

  # in_vars at the command line - precedence 2
  if args.config == '':
    raise TypeError("Config args string is empty")

  if args.config:
    in_vars.update(args.config)

  if args.template == '-':
    # Template passed in via stdin. Create template as a tempfile and use it
    # instead but since includes are possible (though not likely), we have to do
    # this in an isolated tmpdir container to prevent inadvertent reading of
    # includes not meant to be read.
    tmpdir = tempfile.mkdtemp()
    atexit.register(shutil.rmtree, tmpdir)

    _, tmpfile = tempfile.mkstemp(prefix='stdin-', dir=tmpdir, text=True)
    atexit.register(os.remove, tmpfile)

    with open(tmpfile, "a+") as f:
      f.write(sys.stdin.read())
    args.template = tmpfile

  for block in render_template( args.template,
                                in_vars,
                                args.strict_mode ):
    print(block)

