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
from decimal import Decimal
_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 obj.isoformat()
			# 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:
						return str(obj)
						# if hasattr(obj,'__dict__'):
						# 	return obj.__dict__
						# else:
						# 	print("Unknown Type for json")
						# 	print([type(obj),obj])
						# 	return str(obj)
		except Exception as e:
			print(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 toFloat(num:Any):
	match num:
		case None:
			return 0.00
		case float():
			return num
		case _:
			try:
				return float(num)
			except Exception as e:
				return float(0)

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 toMoney(num:Any) -> Decimal:
	try:
		match num:
			case None:
				return Decimal("0.00")
			case _:
				return round(Decimal(str(num)),2)
	except Exception as e:
		return Decimal("0.00")

i = toInt
m = toMoney
s = toStr
f = toFloat

def change(before,after):
	try:
		return (float(after) - float(before)) / float(before)
	except:
		return 0

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 div(left,right,rnd=2):
	try:
		result = round(float(left) / float(right),rnd)
		if rnd == 0:
			return int(result)
		else:
			return float(result)
	except:
		return float(0)

def split(s:str,delim:str) -> list:
	try:
		return s.split(delim)
	except:
		return [s]

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)
		self.utc().pst()
		if tzone:
			self.tz(tzone)

	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 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 {}

