from flask import jsonify
from dataclasses import dataclass,field
import json
from json import dumps,JSONEncoder
from typing import Optional,Any
from datetime import datetime, timedelta,date
import pytz
from dateutil import parser
_debug_log = []

class json_encoder(JSONEncoder):
	def default(self, obj):
		# import Decimal
		# try:
		# Debug(title="JSON Obj type",data=type(obj),force=True)
		try:
			if isinstance(obj, (datetime,date,IODate)):
				return str(obj)
			# elif isinstance(obj, Money):
			# 	return str(obj)
			# elif isinstance(obj, Decimal):
			# 	return str(obj)
			elif isinstance(obj, timedelta):
				if obj.seconds > 0:
					if obj.days:
						return "{} days {} seconds".format(obj.days,obj.seconds)
					else:
						return "{} seconds".format(obj.seconds)
				else:
					return int(obj.days)
			else:
				try:
					return json.JSONEncoder.default(self, obj)
				except:
					try:
						return obj.to_JSON()
					except:
						if hasattr(obj,'__dict__'):
							return obj.__dict__
						else:
							Debug(title="Unknown Type for json",data=[type(obj),obj])
							return str(obj)
		except Exception as e:
			Debug(title="JSON",data=e)

def is_numeric(val):
	try:
		float(val)
		return(True)
	except:
		return(False)

def toList(ldata:Any) -> list:
	match ldata:
		case None:
			return []
		case list():
			return ldata
		case _:
			return [ldata]

def toInt(num:Any):
	match num:
		case None:
			return 0
		case int():
			return num
		case _:
			return int(num)

def toStr(item:Any):	
	match item:
		case None:
			return ""
		case str():
			return item
		case _:
			return str(item)

def toJSON(data):
	try:
		if data is None:
			return ""
		else:
			return dumps(data,skipkeys=True,cls=json_encoder)
			# return json_encoder(ensure_ascii=False).encode(data)
	except Exception as e:
		Debug(title="JSON trouble",data=e)
		Debug(data)
		return str(data)

def getJSON(data,d={}):
	if type(data) == type(d):
		return(data)
	
	if not isinstance(data,str):
		return data

	if not data:
		return d

	try:
		return json.loads(data)

	except Exception as e:
		
		try:
			if data.find("'")>=0:
				import ast
				return ast.literal_eval(data) or d
		except:
			pass		

		Debug(title="Trouble Parsing JSON",data=[type(data),data])
		Debug(title="JSON Error",data=e)

		return d

def first(data, default:Any=""):
	try:
		return data[0]
	except:
		return default

def last(data, default:Any=""):
	try:
		return data[-1]
	except:
		return default

def group_by(field:str, data:[],drop_key:bool=False) -> dict:
	rdata = {}
	for row in data:
		key = row.get(field)
		if drop_key:
			del row[field]
		rdata[key] = row
	return rdata

def tst():
	return f"<h2>Woohoo!</h2><p>Debug:</p>"

def url_encode(data:str) -> str:
	import urllib.parse
	return urllib.parse.quote_plus(data)

def url_decode(data:str) -> str:
	import urllib.parse
	return urllib.parse.unquote(data)

def mk_csv(data,fname=None,delim='	'):
	import csv
	import io
	from io import StringIO ## for Python 3
	csvfile = io.StringIO()
	val = ""
	try:
		assert len(data)>0, "Empty Data"
		cols = data[0].keys()
		writer = csv.DictWriter(csvfile,fieldnames=cols,dialect='excel-tab')
		writer.writeheader()
		for item in data:
			writer.writerow(item)

		val = csvfile.getvalue()
		if fname:
			print("Writing file {}".format(fname))
			with open(fname) as f:
				f.write(val)

	except Exception as e:
		Debug(title="Trouble",data=e)

	return val

def neg(val) -> int:
	try:
		return -abs(val)
	except:
		return val

def parms() -> dict:
	from flask import request
	data = {}

	for k,v in request.form.items():
		data[k]=v

	for k,v in request.args.items():
		data[k]=v
	
	try:
		for f,v in request.files.items():
			data[f] = v
	except Exception as e:
		Debug(title="Trouble with file upload",data=e)

	try:
		for k,v in request.json.items():
			data[k]=v
	except:
		pass
	return data

def errorStr(error) -> str:
	import linecache
	import sys
	exc_type, exc_obj, tb = sys.exc_info()
	f = tb.tb_frame
	lineno = tb.tb_lineno
	filename = f.f_code.co_filename
	linecache.checkcache(filename)
	line = linecache.getline(filename, lineno, f.f_globals)
	e_str = 'EXCEPTION IN ({}, LINE {} "{}"): {}'.format(filename, lineno, line.strip(), exc_obj)
	return e_str

def Debug(title:str="",data:Any | Exception = None,debug:bool=True):
	# global app
	# global _debug_log,app
	import logging
	logger = logging.getLogger('werkzeug') # grabs underlying WSGI logger
	logger.info(data)
	# handler = logging.FileHandler('api.log') # creates handler for the log file
	# logger.addHandler(handler) # adds handler to the werkzeug WSGI logger
	# app.logger.info(data)
	data = data or ""
	if not debug:
		return

	match data:
		case Exception():
			import linecache
			import sys
			exc_type, exc_obj, tb = sys.exc_info()
			f = tb.tb_frame
			lineno = tb.tb_lineno
			filename = f.f_code.co_filename
			linecache.checkcache(filename)
			line = linecache.getline(filename, lineno, f.f_globals)
			d = data
			logger.info('EXCEPTION IN ({}, LINE {} "{}"): {}'.format(filename, lineno, line.strip(), exc_obj))
			
		case dict() | list():
			d = dumps(data,indent=5,ensure_ascii=True,cls=json_encoder)
		case str() | int() | tuple():
			d = data
		case None:
			pass
		case _:
			d = str(data)
	if not d:
		d = str(data)

	print(f"*************************{title}***********************")
	print(d)
	print(f"***************{type(data)}**********************")
	
	logger.info(f"*************************{title}***********************")
	logger.info(d)
	logger.info(f"***************{type(data)}**********************")

@dataclass
class Output():
	data: dict[str,str] = field(default_factory=dict)
	status: int = 1
	message: str = ""

	def __str__(self):
		return self.json
	
	@property
	def obj(self):
		return {"data":self.data,"status":self.status,"message":self.message}

	@property
	def json(self):
		return jsonify(self.obj)

	@property
	def response(self):
		return self.json

class IODate(object):
	quarterDates = {1:("1-1","3-31"),2:("4-1","6-30"),3:("7-1","9-30"),4:("10-1","12-31")}
	dayNameInts = {"Monday":0,"Tuesday":1,"Wednesday":2,"Thursday":3,"Friday":4,"Saturday":5,"Sunday":6}
	monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"]
	shortMonthNames = ["Jan","Feb","Mar","Apr","May","Jun","Ju","Aug","Sep","Oct","Nov","Dec"]
	daylightDelta = 8
	useDayLight = False
	date = None

	def __init__(self,dStr=None,tzone=None):
		self.set(dStr)

		if tzone:
			self.tz(tzone)
		else:
			self.pst()

	def _(self):
		return self.dateStr()

	@property
	def ym(self) -> str:
		return self.dateStr()[0:7]

	@property
	def age(self) -> int:
		return self.daysFromNow(abs=False)

	@property 
	def work_week(self):
		first = self.clone().delta(days=-(self.dayOfWeek()-1))
		last = first.clone().delta(days=6)
		return first,last

	@property
	def work_week_id(self):
		first,last = self.work_week
		return "{}::{}".format(first,last)

	@property
	def week(self):
		return self.date.isocalendar()[1]

	@classmethod
	def list_range(cls,start,end):
		startDate = IODate(start)
		endDate = IODate(end)
		days = []
		while startDate.timestamp <= endDate.timestamp:
			days.append(startDate.dateStr())
			startDate.delta(days=1)
		return days

	@classmethod
	def fromTimestamp(cls,tstamp):
		return IODate(datetime.fromtimestamp((f(tstamp)/1000)))

	@classmethod
	def unStamp(self,seconds):
		hours = int(seconds/3600)
		minutes = int((seconds%3600)/60)
		return {"hours":hours,"minutes":minutes}

	@classmethod
	def unStampStr(cls,seconds):
		t = cls.unStamp(seconds)
		return "{}h {}m".format(t["hours"],t["minutes"])

	@classmethod
	def withtimestamp(cls,timestamp):
		d = datetime.fromtimestamp(timestamp)
		return cls(d)

	@classmethod
	def optional(cls,dateStr):
		if dateStr is not None:
			return IODate(dateStr)
		else:
			return None

	@classmethod
	def monthName(self,num,short=False):
		num = int(num)
		if num> 0 and num < len(self.monthNames):
			if short:
				return self.shortMonthNames[num-1]
			else:
				return self.monthNames[num-1]

	@property
	def quarter(self):
		return (self.month-1)//3 + 1

	@classmethod
	def listMonthRange(self,start=-30,end=0):
		endDate = IODate(end)
		cur = IODate(start)
		done = False
		months = []
		while not done:
			q_str = "{}-{}".format(cur.year,cur.month)

			if (not q_str in months) and int(cur)<=int(endDate):
				months.append(q_str)

			if int(cur)>int(endDate):
				done = True
			cur.delta(days=1)
		return months

	@classmethod
	def listQuarterRange(self,start=-30,end=0):
		# startDate = IODate(start)
		endDate = IODate(end)
		cur = IODate(start)
		done = False
		quarters = []
		while not done:
			quarter = cur.quarter
			q_str = "{}-{}".format(cur.year,quarter)

			if (not q_str in quarters) and int(cur)<=int(endDate):
				quarters.append(q_str)

			if int(cur)>int(endDate):
				done = True
			cur.delta(days=1)
		return quarters

	@classmethod
	def year_range(cls,start,end):
		start = IODate(start)
		end = IODate(end)
		years = []
		for i in range(abs(start.year-end.year)+1):
			years.append(int(start.year)+int(i))
		return years


	def set(self,dStr):
		if isinstance(dStr, IODate):
			self.date = dStr.date
		elif isinstance(dStr,datetime):
			self.date = dStr
		elif is_numeric(dStr):
			self.date = datetime.now()
			self.delta(int(dStr))
		elif dStr:
			try:
				self.date = parser.parse(str(dStr))
			except Exception as e:
				Debug(title=dStr,data=e)
				self.date = datetime.now()
				# self.date = datetime(1977, 11, 9)
				self.pst()
		else:
			self.date = datetime.now()

	def setDate(self,*argc,**argv):
		self.date = self.date.replace(*argc,**argv)
		return self

	def setTime(self,hour=0,minute=0,second=0):
		self.time(hour=hour,minute=minute,second=second)
		return(self)

	def start(self,setDate=False):
		self.time(hour=0,minute=0,second=0)
		return(self)

	def end(self,setDate=False):
		self.time(hour=23,minute=59,second=59)
		return self

	def first_date(self,time=False):
		self.date = self.date.replace(day=1)
		if time:
			self.start()
		return self

	def last_date(self,time=False):
		month_days = self.daysInMonth()
		self.date = self.date.replace(day=month_days)
		if time:
			self.end()
		return self

	def sqlformat(self):
		return(self.date.strftime("%Y-%m-%d %H:%M:%S"))

	def sql(self):
		return(self.sqlformat())

	def __repr__(self):
		return str(self)
	
	def is_between(self,start,end):
		start = IODate(start)
		end = IODate(end)
		return (self.timestamp >=start.timestamp and self.timestamp<=end.timestamp)


	def quarterDate(self,quarter=None,year=None):
		quarter = toInt(quarter,self.quarter)
		year = year if year else self.date.year
		Debug(data = "Quarter: {}".format(quarter),title = "Quarter",force=True)

		if quarter > 4 or quarter < 1:
			Debug(title="quarterDate: bad quarter",data={"quarter":quarter})
			quarter = self.quarter

		start = IODate(datetime.strptime("{} {}".format(year,self.quarterDates[quarter][0]),"%Y %m-%d"))
		end = IODate(datetime.strptime("{} {}".format(year,self.quarterDates[quarter][1]),"%Y %m-%d"))
		Debug(data = "{}=>{}".format(start.dayStr(),end.dayStr()),title = "Start End Dates",force=True)
		return (start,end)

	def getQuarterDates(self,quarter=None):
		q = {1:(1,90),2:(91,180),3:(181,270),4:(271,365)}

		curQ = quarter if quarter else self.quarter
		dates = []
		year = toInt(self.date.strftime("%Y"))

		for i in range(4):
			curQ = curQ - 1
			if curQ == 0:
				year = year - 1
				curQ = 4

			days = q[curQ][0]
			Debug(title="getQuarterDates=>days",data={"days":days})
			dates.append((
					datetime.strptime("{} {}".format(year,self.quarterDates[curQ][0]),"%Y %m-%d"),
					datetime.strptime("{} {}".format(year,self.quarterDates[curQ][1]),"%Y %m-%d")
				))
		return dates


	def time(self,hour=0,minute=0,second=0):
		self.date = self.date.replace(hour=hour,minute=minute,second=second)
		return self

	def clone(self):
		import copy
		return copy.deepcopy(self)

	def dayOfWeek(self):
		return int(self.date.strftime("%u"))

	def dayOfWeekStr(self):
		return self.date.strftime("%a")

	def daysInMonth(self):
		import calendar
		try:
			return calendar.monthrange(self.year, self.month)[1]
		except:
			return -1


	def week_of_year(self):
		return self.date.isocalendar()[1]

	def datetime(self):
		return str(self.date)

	def display_date(self,year=True):
		return self.strftime("%b %e %Y")

	def short_date(self,year=True):
		if year:
			return self.strftime("%y-%m-%d")
		else:
			return self.strftime("%m-%d")

	def dayStr(self,year=True):
		if year:
			return self.date.strftime("%Y-%m-%d")
		else:
			return self.date.strftime("%m-%d")

	def travel(self,days=0,hours = 0, seconds=0,tdelta=None):
		self.date = self.date + timedelta(days = days,hours = hours,seconds=seconds)
		return self

	def delta(self,days=0,hours = 0, seconds=0,minutes=0,tdelta=None,months=0):
		if minutes:
			seconds += minutes*60

		self.date = self.date + timedelta(days = days,hours = hours,seconds=seconds)
		if months>0:
			from dateutil.relativedelta import relativedelta
			self.date = self.date.today() + relativedelta(months=months)
		return self

	def deltaDays(self,dateVal):
		if type(dateVal) is IODate:
			diff = (dateVal.date - self.date).days
		elif type(dateVal) is datetime:
			diff = (dateVal - self.date).days
		else:
			diff = (IODate(dateVal).date-self.date).days
		return diff

	def __add__(self,val):
		if isinstance(val, IODate):
			return self.date + val.date
		elif str(val).find("-")>=1 or str(val).find("/") >=0:
			return self.date + IODate(val).date
		else:
			return IODate(self.date + timedelta(days=val))

	def __sub__(self,val):
		if isinstance(val, IODate):
			return val.date - self.date
		elif str(val).find("-") >=0 or str(val).find("/") >=0:
			return IODate(val).date - self.date
		else:
			return IODate(self.date - timedelta(days=val))
		return self.date

	def __iadd__(self,val):
		if isinstance(val, timedelta):
			self.date = self.date + val
			return self.date
		else:
			self.date = self.date + timedelta(days=val)
		return self

	def __isub__(self,val):
		if isinstance(val, timedelta):
			self.date = self.date - val
			return self.date
		else:
			self.date = self.date - timedelta(days=val)
		return self

	def __eq__(self,val):
		return int(self) == int(val)

	def add(self,val):
		return self.__add__(val)

	def sub(self,val):
		return self.__sub__(val)

	def iadd(self,val):
		return self.__iadd__(val)

	def isub(self,val):
		return self.__isub__(val)

	def to_JSON(self):
		return self.__str__()

	def daysFromNow(self,abs=True):
		today = IODate().pst()
		cdate = self.clone().pst()

		days = (today - cdate).days
		if abs or today < cdate:
			return days
		else:
			return -days

	def today(self):
		return self.dateStr()

	def dateStr(self,year=True):
		if year:
			return str(self.date.strftime("%Y-%m-%d"))
		else:
			return str(self.date.strftime("%m/%d"))

	def datetimeStr(self):
		return str(self.full())

	@classmethod
	def Now(cls):
		return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

	def now(self):
		return self.full()

	def full(self):
		return str(self.date.strftime("%Y-%m-%d %H:%M:%S"))

	def timeStr(self,showSeconds=False,showDelim=True):
		if showSeconds:
			s = "%H:%M:%S"
		else:
			s = "%l:%M"

		if showDelim:
			s+=" %p"

		return self.date.strftime(s)

	def str(self,full=False):		
		if full:
			return self.full()

		return str(self)

	def amz_str(self):
		self.pst().utc()
		return(self.date.strftime("%Y-%m-%dT%H:%M:%SZ"))

	def amz_adjust(self):
		self.pst().utc()
		return self

	def amz_adjust_start(self):
		self.pst().start().utc()
		return self

	def amz_adjust_end(self):
		self.pst().end().utc()
		return self

	def to_local_time(self,mode=None):
		self.utc()
		if mode=='start':
			self.start()
		elif mode =='end':
			self.end()
		return self.pst()

	def to_amz_time(self,mode=None):
		self.pst()
		if mode=='start':
			self.start()
		elif mode =='end':
			self.end()
		return self.utc()

	def str_delta(self,dateStr):
		import parsedatetime as pdt
		from time import mktime
		cal = pdt.Calendar()
		time_struct, parse_status = cal.parse(dateStr,self.date)
		self.date = datetime.fromtimestamp(mktime(time_struct))
		return self

	def __getattr__(self, name):
		return getattr(self.date, name)

	@property
	def timestamp(self):
		return self.__int__()

	def tstamp(self):
		return (self.date - datetime(1970, 1, 1)).total_seconds()

	def __int__(self):
		if self.tzinfo:
			# print("Converting TZ")
			TZ = pytz.timezone('UTC')
			epoch = TZ.localize(datetime(1970, 1, 1))
			return int((self.date - epoch).total_seconds())
		else:
			# print("No TZ")
			return int((self.date - datetime(1970, 1, 1)).total_seconds())

	def __float__(self):
		if self.tzinfo:
			TZ = pytz.timezone('UTC')
			epoch = TZ.localize(datetime(1970, 1, 1))
			return float((self.date - epoch).total_seconds())
		else:
			return float((self.date - datetime(1970, 1, 1)).total_seconds())

	# def __lt__(self,other):
	# 	return float(self) < float(other)

	# def __le__(self,other):
	# 	return float(self) <= float(other)

	# def __eq__(self,other):
	# 	return float(self) == float(other)

	# def __ne__(self,other):
	# 	return not self.__eq__(other)

	# def __gt__(self,other):
	# 	return float(self) > float(other)

	# def __ge__(self,other):
	# 	return float(self) >= float(other)

	def __str__(self):
		return str(self.date.strftime("%Y-%m-%d"))

	def week_day_name(self):
		return self.date.strftime("%a")

	def tz(self,zone):
		TZ = pytz.timezone(zone)
		try:
			self.date = TZ.localize(self.date)
		except Exception as e:
			self.date = self.date.astimezone(TZ)
		return self

	def pst(self):
		try:
			self.tz('US/Pacific')
		except Exception as e:
			pass
		return self

	def utc(self):
		try:
			self.tz('UTC')
		except:
			pass
		return self

	def dst_delta(self):
		return timedelta(hours=IODate.daylightDelta)

	def amazon_time(self):
		return self.date - self.dst_delta()

	def iso_ext(self):
		return self.date.strftime("%Y-%m-%dT%H:%M:%S.%f0%z")

	def amz(self,full=True):
		if full:
			return self.date.isoformat()
		else:
			return self.date.strftime("%Y-%m-%dT%k:%M:%S")


def delta(start,end):
	return ((start - end) / start) * -1

def exception_info(e) -> str:
	import linecache
	import sys
	exc_type, exc_obj, tb = sys.exc_info()
	f = tb.tb_frame
	lineno = tb.tb_lineno
	filename = f.f_code.co_filename
	linecache.checkcache(filename)
	line = linecache.getline(filename, lineno, f.f_globals)
	return(f'EXCEPTION IN ({filename}, LINE {lineno} "{line.strip()}"): {exc_obj}')

def b64_encode(input):
	import base64
	dataBase64 = base64.b64encode(input)
	dataBase64P = dataBase64.decode("UTF-8")
	# Debug(title="base64",data=[dataBase64,dataBase64P])
	return dataBase64P

def b64_decode(input):
	import base64
	return base64.decodebytes(input.encode("ascii"))

def encr(data:dict) -> str:
	from Crypto.Cipher import AES
	from Crypto import Random
	secret = b"7313377313377311"
	j = dumps(data)
	
	s = bytes(j,"UTF-8")
	try:
		if not data:
			return ""
		# data = data.encode('utf-8')

		# iv = Random.new().read(AES.block_size)
		iv = b'\xcf\xbc\x13)"X\xf0\x18]\x9f6n\xe1\xca\xe4\x83'
		cipher = AES.new(secret, AES.MODE_CFB, iv)
		msg = iv + cipher.encrypt(s)
		return b64_encode(msg)
		# return hexlify(msg).decode("utf-8")
	except Exception as e:
		print(e)
		Debug(title="Encrypt",data={"data":data,"Error":e})
		return ""
	return ""

def decr(data:str) -> str:
	from Crypto.Cipher import AES
	from Crypto import Random
	secret = b"7313377313377311"

	d = b64_decode(data)
	try:
		if not data:
			return ""
		# iv = Random.new().read(AES.block_size)
		iv = b'\xcf\xbc\x13)"X\xf0\x18]\x9f6n\xe1\xca\xe4\x83'
		cipher = AES.new(secret, AES.MODE_CFB, iv)
		# msg = cipher.decrypt(codecs.decode(data,"hex"))[len(iv):]
		msg = cipher.decrypt(d)[len(iv):].decode("UTF-8")
		return msg
	except Exception as e:
		print(e)
		Debug(title="Decrypt",data={"data":data,"Error":e})
		return ""
	return {}

###################################
## Config

_config_cache:dict = {}

def config(section:str) -> dict[str,str]:
	"""Read a section out of the .config file that sits next to this module.

	Resolved against __file__ rather than the cwd, so it works no matter what
	WorkingDirectory the service is started with. Cached after the first read.
	"""
	global _config_cache
	if not _config_cache:
		import configparser
		from pathlib import Path
		path = Path(__file__).resolve().parent / ".config"
		parser = configparser.ConfigParser()
		if not path.is_file():
			raise RuntimeError(f"Missing config file: {path}")
		parser.read(path, encoding="utf8")
		_config_cache = {name: dict(parser[name]) for name in parser.sections()}
	return _config_cache.get(section, {})

def config_value(section:str, key:str, required:bool=True) -> str:
	val = config(section).get(key, "")
	if required and not val:
		raise RuntimeError(f"Missing [{section}] {key} in .config")
	return val
