from datetime import datetime, timedelta, timezone
from .bzdata import bzdata
import pytz
from . import bz24

gGodstem = {0: "甲", 1: "乙", 2: "丙", 3: "丁", 4: "戊", 5: "己", 6: "庚", 7: "辛", 8: "壬", 9: "癸"}
gEarthstem = {0: "子", 1: "丑", 2: "寅", 3: "卯", 4: "辰", 5: "巳", 6: "午", 7: "未", 8: "申", 9: "酉", 10: "戌", 11: "亥"}
gEarthAnimal = {0: "鼠", 1: "牛", 2: "虎", 3: "兔", 4: "龙", 5: "蛇", 6: "马", 7: "羊", 8: "猴", 9: "鸡", 10: "狗", 11: "猪"}
gYellowBlackPath = {0: "青龙", 1: "明堂", 2: "天刑", 3: "朱雀", 4: "金匮", 5: "天德", 6: "白虎", 7: "玉堂", 8: "天牢", 9: "玄武", 10: "司命", 11: "勾陈"}
gYellowBlackPathGoodBad = {
    # Good gods (黄道): 青龙、明堂、金匮、天德、玉堂、司命
    "青龙": "good",
    "明堂": "good", 
    "金匮": "good",
    "天德": "good",
    "玉堂": "good",
    "司命": "good",
    # Bad gods (黑道): 天刑、朱雀、白虎、天牢、玄武、勾陈
    "天刑": "bad",
    "朱雀": "bad",
    "白虎": "bad", 
    "天牢": "bad",
    "玄武": "bad",
    "勾陈": "bad"
}
gEarthElement = {
    0: {"n": "子", "e": "water"},
    1: {"n": "丑", "e": "earth"},
    2: {"n": "寅", "e": "wood"},
    3: {"n": "卯", "e": "wood"},
    4: {"n": "辰", "e": "earth"},
    5: {"n": "巳", "e": "fire"},
    6: {"n": "午", "e": "fire"},
    7: {"n": "未", "e": "earth"},
    8: {"n": "申", "e": "metal"},
    9: {"n": "酉", "e": "metal"},
    10: {"n": "戌", "e": "earth"},
    11: {"n": "亥", "e": "water"}
}
gStar = {1: "1白", 2: "2黑", 3: "3碧", 4: "4绿", 5: "5黄", 6: "6白", 7: "7赤", 8: "8白", 9: "9紫"}
gStarMonthData = {'year': 2023, 'month': 2, 'star': 8}
gJianChu12God = {0: "建", 1: "除", 2: "满", 3: "平", 4: "定", 5: "执", 6: "破", 7: "危", 8: "成", 9: "收", 10: "开", 11: "闭"}
gJianChu12Data = {'date': datetime(2023, 2, 13), 'god': 0}#13/2/2023 建日
g12ChangSheng = {0: "长生", 1: "沐浴", 2: "冠带", 3: "临官", 4: "帝旺", 5: "衰", 6: "病", 7: "死", 8: "墓", 9: "绝", 10: "胎", 11: "养"}
g12CSby5E = {0: 11, 1: 2, 2: 8, 3: 5, 4: 8}#0（木）长生在亥（11），1（火）长生在寅（2），2（土）长生在申（8），3（金）长生在巳（5），4（水）长生在申（8）
g12CSbyGod = {0: 11, 1: 6, 2: 2, 3: 9, 4: 2, 5: 9, 6: 5, 7: 0, 8: 8, 9: 3}#0（甲）长生在亥（11），1（乙）6（午），2（丙）2（寅），3（丁）酉（9），4（戊）寅（2），5（己）酉（9），6（庚）巳（5），7（辛）子（0），8（壬）申（8），9（癸）卯（3）
gGLen = len(gGodstem)
gELen = len(gEarthstem)
g5Elements = {0:"wood",1:"fire",2:"earth",3:"metal",4:"water"}
g5ELen = 5
g5ERel = {0:"same",1:"iproduce",2:"icounter",3:"counterme",4:"produceme"}
gStartData = {'date': datetime(2023, 2, 4, 0, 0),'god': 9,'earth': 5}#4/2/2023 9:癸 5:巳
gStartTime = {'date': datetime(2023, 2, 4, 3),'god': 0,'earth': 2}#4/2/2023 3:00 0:甲 2:寅
gGodstem5Element = {
    0: {"e": "wood", "t": 1}, 1: {"e": "wood", "t": 0}, #"t" means type, 1 - yang, 0 - yin
    2: {"e": "fire", "t": 1}, 3: {"e": "fire", "t": 0},
    4: {"e": "earth", "t": 1}, 5: {"e": "earth", "t": 0},
    6: {"e": "metal", "t": 1}, 7: {"e": "metal", "t": 0},
    8: {"e": "water", "t": 1}, 9: {"e": "water", "t": 0}
}
g10GodRel = {
    0: {"same": "比肩", "diff": "劫财"}, #the index 0 follow the g5ERel, 0-same, 1-iproduce, 2-icounter, 3-counterme, 4-produceme
    1: {"same": "食神", "diff": "伤官"},
    2: {"same": "偏财", "diff": "正财"},
    3: {"same": "七煞", "diff": "正官"},
    4: {"same": "偏印", "diff": "正印"}
}
gEarthstemGod = {
    0: {0: 9}, #子-癸
    1: {0: 5, 1: 9, 2: 7}, #丑-己癸辛
    2: {0: 0, 1: 2, 2: 4}, #寅-甲丙戊
    3: {0: 1}, #卯-乙
    4: {0: 4, 1: 1, 2: 9}, #辰-戊乙癸
    5: {0: 2, 1: 6, 2: 4}, #巳-丙庚戊
    6: {0: 3, 1: 5}, #午-丁己
    7: {0: 5, 1: 3, 2: 1}, #未-己丁乙
    8: {0: 6, 1: 8, 2: 4}, #申-庚壬戊
    9: {0: 7}, #酉-辛
    10: {0: 4, 1: 7, 2: 3}, #戌-戊辛丁
    11: {0: 8, 1: 0}, #亥-壬甲
}
gShaGod = {0: "天乙贵人", 1: "天德贵人", 2: "月德贵人", 3: "太极贵人", 4: "禄神", 5: "文昌", 6: "驿马", 7: "桃花", 8: "天喜星", 9: "红鸾", 10: "孤辰寡宿", 11: "劫煞", 12: "亡神", 13: "羊刃", 14: "天医", 15: "元辰", 16: "阴差阳错", 17: "灾煞", 18: "华盖", 19: "谋星", 20: "天贼", 21: "天赦"}

gLifePalace = {1: 0, 3: 0, 4: 0, 9: 0, 2: 1, 6: 1, 7: 1, 8: 1} #东四命 (0): 1,3,4,9 西四命 (1): 2,6,7,8

# gMYTimezone = pytz.timezone('Asia/Kuala_Lumpur')
gMYTimezone = pytz.FixedOffset(480)

def beforeLiChun(year, month, day, hour=0, minute=0, second=0):
    before = False
    if year in bzdata:
        # tz = pytz.FixedOffset(480)
        date = datetime(year, month, day, hour, minute, second, tzinfo=gMYTimezone)
        lc = datetime.strptime(bzdata[year][2], '%Y-%m-%d %H:%M:%S')
        lc = lc.replace(tzinfo=timezone.utc)
        if date < lc:
            before = True
    elif month * 100 + day < 204:
        before = True
    return before

def getGodStemYear(year, month, day, hour=0, minute=0, second=0):
    yi = year
    if beforeLiChun(year, month, day, hour, minute, second):
        yi -= 1
    i = (int(str(yi)[-1]) + 7)
    if i > 10:
        i -= 10
    return i - 1

def getEarthStemYear(year, month, day, hour=0, minute=0, second=0):
    """
    Calculate the Earth Stem (地支) for a given date and time.

    The Earth Stem represents the 12 terrestrial branches in traditional Chinese astrology.
    This function calculates the Earth Stem based on the given date and time, taking into account the intercalary month (閏月).

    Parameters:
    year (int): The year of the date.
    month (int): The month of the date.
    day (int): The day of the date.
    hour (int, optional): The hour of the time. Default is 0.
    minute (int, optional): The minute of the time. Default is 0.
    second (int, optional): The second of the time. Default is 0.

    Returns:
    int: The index number of the Earth Stem (地支, 0-11).

    Example:
    >>> getEarthStemYear(2022, 2, 15)
    1  # 2022-02-15 is 寅 (Earth Stem 2)
    """
    yi = year
    if beforeLiChun(year, month, day, hour, minute, second):
        yi -= 1
    i = (yi - 1900) % gELen
    if i < 0:
        i += gELen
    return i

def getHourGodEarthStem(year, month, day, hour, minute=0, second=0):
    """
    Calculate the God Stem (天干) and Earth Stem (地支) for a given hour.

    This function determines the God Stem and Earth Stem based on the Chinese astrological system
    for the specified date and time.

    Parameters:
    year (int): The year of the date.
    month (int): The month of the date (1-12).
    day (int): The day of the month.
    hour (int): The hour of the day (0-23).
    minute (int, optional): The minute of the hour. Defaults to 0.
    second (int, optional): The second of the minute. Defaults to 0.

    Returns:
    dict: A dictionary containing two keys:
        'g': The index of the God Stem (0-9)
        'e': The index of the Earth Stem (0-11)

    Note:
    - The Earth Stem changes every 2 hours.
    - The God Stem is calculated based on a predefined start time (gStartTime).
    """
    # Find earth
    hi = hour
    if hour == 23:
        hi = 0
    ei = (hi + 1) // 2  # Moves every 2 hours

    # Find god
    time = datetime(year, month, day, hour)
    diff = time - gStartTime['date']
    move = diff.total_seconds() // (3600 * 2)  # Moves every 2 hours
    move = int(move % gGLen)
    if move < 0:
        move += gGLen
    gi = (gStartTime['god'] + move) % gGLen

    return {'g': gi, 'e': ei}

def getYearGodEarthStem(year, month, day, hour=0, minute=0, second=0):
    return {'g': getGodStemYear(year, month, day, hour, minute, second),
            'e': getEarthStemYear(year, month, day, hour, minute, second)}

def getDayGodEarthStem(year, month, day, hour=0, minute=0, addtime=0):
    date = datetime(year, month, day) + timedelta(seconds=addtime)
    diff = (date - gStartData['date']).days

    goddiff = diff % gGLen
    if goddiff < 0:
        goddiff += gGLen
    god_i = (gStartData['god'] + goddiff) % gGLen

    earthdiff = diff % gELen
    if earthdiff < 0:
        earthdiff += gELen
    earth_i = (gStartData['earth'] + earthdiff) % gELen

    return {'g': god_i, 'e': earth_i}

def getMonthEarthStem(year, month, day, hour=0, minute=0, second=0):
    earth_found = False
    mi = month % 12  # 0 means December
    if year in bzdata:
        bz = datetime.strptime(bzdata[year][mi], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
    else:
        bz12 = bz24.calc12Cycle(year)
        bz = datetime.strptime(bz12[mi]['d'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)

    if bz:
        date = datetime(year, month, day, hour, minute, second, tzinfo=gMYTimezone)
        if date >= bz:
            earth = mi
        else:
            earth = mi - 1
            if earth < 0:
                earth += gELen
        earth_found = True

    if not earth_found:
        earthdate = {
            0: 106,
            1: 204,
            2: 306,
            3: 406,
            4: 505,
            5: 606,
            6: 708,
            7: 809,
            8: 908,
            9: 1008,
            10: 1108,
            11: 1207,
            12: 1306,  # January
        }
        md = month * 100 + day
        for i in range(len(earthdate) - 1):
            if md < earthdate[0]:
                earth = 11  # December
                break
            elif earthdate[i] <= md < earthdate[i + 1]:
                earth = i
                break
        earth = (earth + 1) % 12

    return earth

def findGodI(god):
    for i in range(len(gGodstem)):
        if god == gGodstem[i]:
            return i
    return None

def findEarthI(earth):
    for i in range(len(gEarthstem)):
        if earth == gEarthstem[i]:
            return i
    return None

def getMonthGodEarthStem(year, month, day, hour=0, minute=0, second=0):
    yi = year
    if beforeLiChun(year, month, day, hour, minute, second):
        yi -= 1
    i = int(str(yi)[-1])
    i = i * 2 + 5
    yingod_i = int(str(i)[-1]) - 1
    me = monthearth = getMonthEarthStem(year, month, day, hour, minute, second)
    yin_i = findEarthI("寅")
    if me < yin_i:
        me += gELen
    diff_to_move = me - yin_i
    move = yingod_i + diff_to_move
    monthgod_i = move % gGLen
    return {"g": monthgod_i, "e": monthearth}

def getDateTimeGodEarthStem(year, month, day, hour, minute=0, second=0):
    addtime = 86400 if hour == 23 else 0
    yge = getYearGodEarthStem(year, month, day, hour, minute, second)
    mge = getMonthGodEarthStem(year, month, day, hour, minute, second)
    dge = getDayGodEarthStem(year, month, day, hour, minute, second+addtime)
    hge = None
    if hour is not None:
        hge = getHourGodEarthStem(year, month, day, hour, minute, second)

    return {"hour": hge, "day": dge, "month": mge, "year": yge}

"""
Calculate the bazi (god/earth stem for year/month/day/hour) for use in the 10k calendar display, this is to fix
the issue when some of the 24 cycles time falls after 23:00, when it does, the day move forward 1 day and its
god/earth stem for the day will be incorrect, so when this happen, the god/earth stem for the day will remain
without 1 day forward.

Args:
    year (int): the year
    month (int): the month, (1 to 12)
    day (int): the day
    hour (int): the hour (0 to 23)
    minute (int): the minute (0 to 59)
    second (int): the minute (0 to 59)
Return:
    dict:
        'year'/'month'/'day'/'hour':
            'g': the god index
            'e': the earth index
"""
def getCalendar10kGodEarthStem(year, month, day, hour, minute=0, second=0):
    bz = getDateTimeGodEarthStem(year, month, day, hour, minute, second)
    #because when 24 cycle time falls after 23:00, the god/earth stem for the day will move forward 1 day but in calendar display it should follow the same day (not 1 day after)
    if(hour == 23):
        daybz = getDateTimeGodEarthStem(year, month, day, 0, minute, second)
        bz['day'] = daybz['day']
    return bz

# bz = getDateTimeGodEarthStem(2023,8,2,0,1)
# print(gGodstem[bz['year']['g']]+gEarthstem[bz['year']['e']]+' '+gGodstem[bz['month']['g']]+gEarthstem[bz['month']['e']]+' '+gGodstem[bz['day']['g']]+gEarthstem[bz['day']['e']]+' '+gGodstem[bz['hour']['g']]+gEarthstem[bz['hour']['e']])

def calcEarthEmpty(god, earth):
    move = gGLen - god
    empty = (earth + move) % gELen
    return {0: empty, 1: (empty + 1) % gELen}

def findElementI(element):
    for i in range(len(g5Elements)):
        if g5Elements[i] == element:
            return i
    return None

def is6Harmony(e1, e2):
    harmony = {0: 1, 1: 0, 2: 11, 3: 10, 4: 9, 5: 8, 6: 7, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2}
    return harmony[e1] == e2

def is3Harmony(e1, e2, e3):
    yes = False
    harmony = {0: {0: 0, 1: 4, 2: 8}, 1: {0: 1, 1: 5, 2: 9}, 2: {0: 2, 1: 6, 2: 10}, 3: {0: 3, 1: 7, 2: 11}}
    if e1 != e2 and e1 != e3 and e2 != e3:
        for i in harmony:
            if not yes:
                h = {}
                for j in harmony[i]:
                    if harmony[i][j] == e1:
                        h[e1] = 1
                    elif harmony[i][j] == e2:
                        h[e2] = 1
                    elif harmony[i][j] == e3:
                        h[e3] = 1
                if len(h) == 3:  # harmony found
                    yes = True
    return yes

def isRelClash(e1, e2):
    return ((e1 + 6) % gELen) == e2

def is6Clash(e1, e2):
    """Alias for isRelClash - checks if two earth branches form 六冲 (Six Clash)"""
    return isRelClash(e1, e2)

def calcElementRel(e1, e2):
    mi = e1
    oi = e2
    diff = oi - mi

    if abs(diff) > 2:
        if diff > 0:
            mi += g5ELen
        else:
            oi += g5ELen

    diff = oi - mi

    if diff == 0:
        rel = 0  # 同我
    elif diff == 1:
        rel = 1  # 我生
    elif diff == 2:
        rel = 2  # 我克
    elif diff == -2:
        rel = 3  # 克我
    else:
        rel = 4  # 生我

    return rel

#is earth1 counter earth2 (is earth2 countered by earth1)
def isRelCounter(e1, e2):
    return calcElementRel(findElementI(gEarthElement[e1]['e']), findElementI(gEarthElement[e2]['e'])) == 2  # icounter

#is earth1 produce earth2 (is earth2 produced by earth1)
def isRelProduce(e1, e2):
    return calcElementRel(findElementI(gEarthElement[e1]['e']), findElementI(gEarthElement[e2]['e'])) == 1  # iproduce

#is earth1 same with earth2
def isRelSame(e1, e2):
    return calcElementRel(findElementI(gEarthElement[e1]['e']), findElementI(gEarthElement[e2]['e'])) == 0  # same

#is earth1 harm (害) with earth2
def isRelHarm(e1, e2):
    #{0: "子", 1: "丑", 2: "寅", 3: "卯", 4: "辰", 5: "巳", 6: "午", 7: "未", 8: "申", 9: "酉", 10: "戌", 11: "亥"}
    harm = {0:7,1:6,2:5,3:4,4:3,5:2,6:1,7:0,8:11,9:10,10:9,11:8}
    return harm[e1] == e2

#is earth1 punish (刑) with earth2/earth3
def isRelPunish(e1, e2, e3 = None):
    #{0: "子", 1: "丑", 2: "寅", 3: "卯", 4: "辰", 5: "巳", 6: "午", 7: "未", 8: "申", 9: "酉", 10: "戌", 11: "亥"}
    pun = {0:3,3:0,4:4,6:6,9:9,11:11}#子卯刑、辰辰自刑、午午自刑、酉酉自刑、亥亥自刑
    pun3 = {2:{5,8},5:{2,8},8:{2,5},1:{7,10},7:{1,10},10:{1,7}}#寅巳申三刑、丑未戌三刑
    if e3 is not None:
        return e1 in pun3 and e2 in pun3[e1] and e3 in pun3[e1]
    else:
        return e1 in pun and pun[e1] == e2

"""
get the 5 elements (五行) for a given earth (地支)
Args:
    earth (int): the number representing the earth (0 to 11, refer gEarthstem)
Return:
    string: the 5 element (wood/fire/earth/metal/water)
"""
def getEarthElement(earth):
    return gEarthElement[earth]['e'] if earth in gEarthElement else None

"""
get the 5 elements number (五行) for a given earth (地支)
Args:
    earth (int): the number representing the earth (0 to 11, refer gEarthstem)
Return:
    int: the 5 element index number (g5Elements, wood/fire/earth/metal/water)
"""
def getEarthElementI(earth):
    return findElementI(getEarthElement(earth))

"""
get the animal sign from the earth (地支)
"""
def getEarthAnimal(earth):
    return gEarthAnimal[earth]

def calcYearFlyingStar(year):
    total = sum(int(i) for i in str(year))
    while total > 10:
        total = sum(int(i) for i in str(total))
    star = 11 - total
    return star

def calcMonthFlyingStar(year, month):
    diff = (year * 12 + month) - (gStarMonthData['year'] * 12 + gStarMonthData['month'])
    star = gStarMonthData['star'] - diff
    star = star % 9 or 9
    return star

def getStarName(star):
    return gStar[star]

def calcJianChu12God(year, month, day, bz=[]):
    if day not in bz or month not in bz:
        date = datetime(year, month, day)
        bz = getDateTimeGodEarthStem(year, month, day, 23, 59, 59)
        bz['day']['e'] = (bz['day']['e'] - 1) % 12#since after 11pm the earth will forward 1 day
    diff = bz['day']['e'] - bz['month']['e']
    god = diff % 12
    return god

def getJianChu12God(god):
    return gJianChu12God[god]

"""
Calculate the 12 Chang Seng for the given 5 element (wood/fire/earth/metal/water, 0-4) at the given earth (地支, 0-11)
Args:
    element (int): the 5 element index number (g5Elements)
    atEarth (int): the earth index number (gEarthstem)
Return:
    int: the 12 Chang Seng number (g12ChangSeng)
"""
def calc12ChangShengBy5E(element, atEarth):
    cs = g12CSby5E[element]
    return (atEarth - cs) % 12

"""
Calculate the 12 Chang Seng for the given god (天干, 0-9) at the given earth (地支, 0-11)
Args:
    god (int): the god index number (gGodstem)
    atEarth (int): the earth index number (gEarthstem)
Return:
    int: the 12 Chang Seng number (g12ChangSeng)
"""
def calc12ChangSengByGod(god, atEarth):
    cs = g12CSbyGod[god]
    if((god + 1) % 2 == 1):#odd number means 阳干
        pos = (atEarth - cs) % 12
    else:
        pos = (cs - atEarth) % 12
    return pos

"""
get the 12 Chang Seng name by its index number
Args:
    cs (int): the index number for 12 Chang Seng (g12ChangSeng)
Return:
    string: the name of the 12 Chang Seng
"""
def get12ChangSeng(cs):
    return g12ChangSheng[cs]

"""
find the index number from the 12 Chang Seng text
Args:
    cs (string): the name of the 12 Chang Seng (eg: 帝旺)
Return:
    int: the index number of the 12 Chang Seng (g12ChangSeng)
"""
def find12ChangSengI(cs):
    for i in range(len(g12ChangSheng)):
        if g12ChangSheng[i] == cs:
            return i
    return None

def findGodstem5E(god):
    """
    Find the 5 elements (wood/fire/earth/metal/water) and type (阳/阴) for a given god (天干, 0-9).

    Parameters:
    god (int): The index number of the god (gGodstem).

    Returns:
    dict: A dictionary containing the 5 element and type for the given god. If the god is not found, return None.

    Example:
    >>> findGodstem5E(0) #0 means 甲
    {'e': 'wood', 't': '1'} #"t" means type, 1 means yang, 0 means yin
    >>> findGodstem5E(5) #5 means 己
    {'e': 'earth', 't': '0'}
    >>> findGodstem5E(10)
    None
    """
    for i in range(len(gGodstem5Element)):
        if i == god:
            return gGodstem5Element[i]

    return None

def calc10God(god, againstGod):
    """
    Calculate the 10 God (十神) for a given god (天干, 0-9) against another god.

    The 10 God is a concept in Chinese astrology, representing the influence of different gods on each other.
    This function calculates the 10 God based on the 5 elements (wood/fire/earth/metal/water) and type (阳/阴)
    of the given god and the god it is being compared against.

    Parameters:
    god (int): The index number of the god (gGodstem) for which the 10 God is to be calculated.
    againstGod (int): The index number of the god (gGodstem) against which the given god is being compared.

    Returns:
    str: The 10 God for the given god against the given againstGod.

    Example:
    >>> calc10God(0, 5)  # 0 means 甲, 5 means 己
    '正财'
    >>> calc10God(5, 0)  # 5 means 己, 0 means 甲
    '正官'
    >>> calc10God(9, 1)  # 10 means 癸, 1 means 乙
    '食神'
    """
    god5E = findGodstem5E(god)
    aGod5E = findGodstem5E(againstGod)
    rel = calcElementRel(findElementI(god5E['e']), findElementI(aGod5E['e']))
    if god5E['t'] == aGod5E['t']:
        tenGod = g10GodRel[rel]['same']
    else:
        tenGod = g10GodRel[rel]['diff']
    return tenGod

def findEarthstemGod(earth):
    """
    Find the god(s) (天干, 0-9) for a given earth (地支, 0-11).

    Parameters:
    earth (int): The index number of the earth (gEarthstem).

    Returns:
    dict: A dictionary containing the god(s) for the given earth stem. If the earth is not found, return None.

    Example:
    >>> findEarthstemGod(0) #0 means 子, will return {0: 9} #9 means 癸
    >>> findEarthstemGod(11) #8 means 申, will return {0: 6, 1: 8, 2: 4} #庚壬戊
    >>> findEarthstemGod(12)
    None
    """
    for i in range(len(gEarthstemGod)):
        if i == earth:
            return gEarthstemGod[i]

    return None

def calcEarthstem10God(god, earth):
    """
    Calculate the 10 Gods (十神) for a given god (天干) against the god(s) associated with a given earth stem (地支).

    This function determines the 10 God relationships between a specified god and the god(s) associated
    with a given earth stem. It uses the findEarthstemGod and calc10God functions to perform the calculations.

    Parameters:
    god (int): The index number of the god (天干, 0-9) for which the 10 God relationships are to be calculated.
    earth (int): The index number of the earth stem (地支, 0-11) whose associated god(s) will be compared against.

    Returns:
    dict or None: A dictionary where keys are the associated god indices of the earth stem,
                  and values are their respective 10 God relationships with the given god.
                  Returns None if no associated gods are found for the given earth stem.

    Example:
    >>> calcEarthstem10God(0, 2)  # 0 (甲) against 2 (寅)
    {0: '比肩', 2: '食神', 4: '偏财'} #0 - 甲, 2 - 丙, 4 - 戊
    """
    gods = findEarthstemGod(earth)
    if gods:
        #loop thru the gods, gods is the dictionary eg {0: 1, 1:8}
        tengods = {}
        for k, v in gods.items():
            tengod = calc10God(god, v)
            tengods[v] = tengod
        return tengods
    return None

def calcLifeNumber(gender, year, month, day, hour, minute, second = 0):
    isLiChun = not beforeLiChun(year, month, day, hour, minute, second)
    if isLiChun:
        calcYear = year
    else:
        calcYear = year - 1

    # number = split the year into single digit and sum it out, do so until it is less than 11
    digitSum = calcYear
    while digitSum >= 11:
        digitSum = sum(int(digit) for digit in str(digitSum))
    
    # Use modulo to get a number between 1-9 by cycling through
    # First get the remainder when divided by 9
    # remainder = digitSum % 9
    
    # If remainder is 0, set it to 9
    midNumber = 11 - digitSum
    
    number = 0 # just in case
    if gender == 'm':
        number = midNumber
        if midNumber == 5:
            number = 2
    elif gender == 'f':
        if midNumber == 1:
            number = 8
        elif 2 <= midNumber <= 5:
            number = 6 - midNumber
        else: # 6 - 9
            number = 10 - midNumber + 5
    
    # Ensure number is within valid range (1-9)
    if number < 1 or number > 9:
        # Default to 1 if out of range
        number = 1
        
    return number

def calcLifePalace(gender, year, month, day, hour, minute, second):
    number = calcLifeNumber(gender, year, month, day, hour, minute, second)
    return gLifePalace[number]

def calcBazi(year, month, day, hour=None, minute=None, second=None, useLastSecondIfTimeNA=True):
    bazi = getDateTimeGodEarthStem(
        year, 
        month, 
        day, 
        hour if hour is not None else (22 if useLastSecondIfTimeNA else 0), 
        minute if minute is not None else (59 if useLastSecondIfTimeNA else 0), 
        second if second is not None else (59 if useLastSecondIfTimeNA else 0)
    )

    return bazi

def calc10YearsFate(gender, column, year, month, day, hour=None, minute=None, second=None):
    """
    Calculate the 10-year fate cycle for a given person based on their birth date and gender.

    This function determines the starting years for the first 10-year fate cycle,
    the direction of the cycle (forward or backward), and the number of days between the 
    person's birthday and the next cycle (depending on forward/backward). The calculation is
    based on Chinese astrology principles.

    Parameters:
    gender (str): The gender of the person ('m' for male, 'f' for female).
    column (int): The number of columns (10-year cycles) to calculate.
    year (int): The birth year of the person.
    month (int): The birth month of the person (1-12).
    day (int): The birth day of the person.
    hour (int, optional): The birth hour of the person (0-23). Defaults to None.
    minute (int, optional): The birth minute of the person (0-59). Defaults to None.
    second (int, optional): The birth second of the person (0-59). Defaults to None.

    Returns:
    tuple: A tuple containing four elements:
           - int: The number of years for the first 10-year fate cycle.
           - int: The direction of the cycle (1 for forward, -1 for backward).
           - int: The number of days between the person's birthday and the next cycle.
           - list: A list of dictionaries, each representing a 10-year cycle column with keys:
                   'g': God stem index
                   'e': Earth branch index
                   'y': Starting year of the cycle

    Note:
    If no birth time is specified and the birthday falls on the day when the cycle starts,
    it is assumed that the days = 0, and hence years = 0.
    """
    # when constructing date without birth time, we just use time as 00:00:00 since comparison later will only use the date part.
    date = datetime(year, month, day, hour or 0, minute or 0, second or 0, tzinfo=gMYTimezone)
    # when calculating the bazi of the birthday without birth time, we use the last second of the day (22:59:59) so that the bazi date will always after the cycle start time, so that we can assume birthday that falls on cycle date will always have the bazi calculated after the cycle start time (to prevent cases where if we assume 00:00:00 and cycle start time is 14:00:00 then it will most likely have the bazi calculated before the cycle start time, which will be calculated as 1 day before), as this will affect the god stem and earth branch of the year being calculated.
    bazi = getDateTimeGodEarthStem(year, month, day, hour or 22, minute or 59, second or 59)
    # determine forward / backward
    if (gender == 'm' and bazi['year']['e'] % 2 == 0) or (gender == 'f' and bazi['year']['e'] % 2 == 1): #male with yang earth / female with yin earth
        direction = 'forward'
    else:
        direction = 'backward'
    # calculate number of days for next cycle (forward) or prev cycle (backward)
    cycle12 = bz24.calc12Cycle(year) #cycle datetime in utc+0
    cycleDate = datetime.strptime(cycle12[(month%12)]['d'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc).astimezone(gMYTimezone)
    diff = date.date() - cycleDate.date()
    days = diff.total_seconds() / 86400
    if (direction == 'forward' and days <= 0) or (direction == 'backward' and days >= 0):
        years = round(abs(days) / 3)
    elif hour is None and date.day == cycleDate.day: # hour is not provided and fall on the same day, assume 0 days
        years = 0
    else:
        if direction == 'forward': # get next cycle
            if month == 12: # 12 means december
                nextCycle12 = bz24.calc12Cycle(year+1)
                nextPrevCycle = nextCycle12[1] #january
            else:
                nextPrevCycle = cycle12[(month+1)%12]
        else: # get prev cycle
            if month == 1: # 1 means january
                prevCycle12 = bz24.calc12Cycle(year-1)
                nextPrevCycle = prevCycle12[0] # december
            else:
                nextPrevCycle = cycle12[(month-1)%12]
        nextPrevDate = datetime.strptime(nextPrevCycle['d'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc).astimezone(gMYTimezone)
        diff = date.date() - nextPrevDate.date()
        days = diff.total_seconds() / 86400
        years = round(abs(days) / 3)

    columns = []
    baseG = bazi['month']['g']
    baseE = bazi['month']['e']
    baseY = years
    for i in range(column):
        if direction == 'forward':
            baseG += 1
            baseE += 1
        else:
            baseG -= 1
            baseE -= 1
        g = baseG % gGLen
        e = baseE % gELen
        y = baseY
        baseY += 10
        columns.append({'g': g, 'e': e, 'y': y})

    return years, 1 if direction == 'forward' else -1, round(abs(days)), columns

def calcNominalAge(year, month, day, hour=None, minute=None, second=None, currentYearIgnoreLichun=False, dateNow=None):
    """
    Calculate the nominal age based on the Chinese age reckoning system.

    This function calculates the nominal age of a person based on their birth date
    and the current date, taking into account the Lichun (立春) solar term.

    Parameters:
    year (int): The birth year.
    month (int): The birth month (1-12).
    day (int): The birth day of the month.
    hour (int, optional): The birth hour (0-23). Defaults to None.
    minute (int, optional): The birth minute (0-59). Defaults to None.
    second (int, optional): The birth second (0-59). Defaults to None.
    currentYearIgnoreLichun (bool, optional): If True, ignores Lichun for the current year. Defaults to False.
    dateNow (datetime, optional): The reference date for age calculation. Defaults to current date and time.

    Returns:
    int: The calculated nominal age according to the Chinese age reckoning system.
         This age is typically one or two years older than the Western age calculation.

    Note:
    If no birth time is specified and the birthday falls on the Lichun (立春) day, 
    it is always assumed that the person's birthday is after the Lichun (立春) time.
    """
    now = datetime.now(pytz.utc).astimezone(gMYTimezone) if dateNow is None else dateNow
    age = now.year - year + 1
    # when a person birthday is before lichun, its age will increment by 1
    if beforeLiChun(year, month, day, 22 if hour is None else hour, 59 if minute is None else minute, 59 if second is None else second):
        age += 1
    # if current year is not lichun yet, deduct by 1
    if not currentYearIgnoreLichun and beforeLiChun(now.year, now.month, now.day, now.hour, now.minute, now.second):
        age -= 1

    return age

def calcYearFate(totalYear, yearNow=None):
    """
    Calculate the yearly fate (Bazi) for a specified number of years starting from a given year.

    This function computes the Bazi (Chinese astrology elements) for each year in a sequence,
    starting from the specified year or the current year if not provided.

    Parameters:
    totalYear (int): The number of years to calculate the fate for.
    yearNow (int, optional): The starting year for calculations. If not provided, the current year is used.

    Returns:
    list: A list of dictionaries, where each dictionary contains a year as the key and its corresponding
          Bazi elements as the value. The Bazi elements are represented as a dictionary with 'g' (god Stem)
          and 'e' (Earthly Branch) keys.

    Note:
    - The year's Bazi is calculated using the calcBazi function, assuming it's after Lichun (立春).
    """
    # Get the current year if yearNow is not provided
    year = datetime.now(pytz.utc).astimezone(gMYTimezone).year if yearNow is None else yearNow

    # Generate years from yearNow up to (yearNow + totalYear - 1)
    # Ensure totalYear is at least 1
    years = [year + i for i in range(max(1, totalYear))]

    columns = []
    bazi = None  # Store the Bazi of the first year

    for i, year in enumerate(years):
        if not bazi:
            # Calculate Bazi for the first year
            bazi = calcBazi(year, 3, 1)  # Always assume it is after Lichun
            columns.append({year: bazi['year']})
        else:
            # Get previous year's Bazi values
            prev_bazi = list(columns[i - 1].values())[0]
            g = (prev_bazi['g'] + 1) % 10
            e = (prev_bazi['e'] + 1) % 12
            columns.append({year: {'g': g, 'e': e}})

    return columns

def calcChineseZodiac(year, month, day, hour=None, minute=None, second=None):
    bazi = calcBazi(year, month, day, hour, minute, second)
    return bazi['year']['e']

def calcFiveElementStrengths(bazi_data):
    """
    Calculate the strength of each of the five elements in the BaZi chart.
    
    Args:
        bazi_data (dict): Dictionary containing year, month, day, and hour pillar data
        
    Returns:
        dict: Dictionary with element names as keys and their strength values
    """
    # Initialize element counts
    elements = {
        'wood': 0,
        'fire': 0,
        'earth': 0,
        'metal': 0,
        'water': 0
    }
    
    # Process each pillar to accumulate element strengths
    pillars = ['year', 'month', 'day', 'hour']
    
    for pillar_name in pillars:
        if pillar_name in bazi_data and bazi_data[pillar_name]:
            pillar_data = bazi_data[pillar_name]
            
            # Add god stem's element (main stem)
            god_stem_idx = pillar_data['g']
            god_stem_5e = findGodstem5E(god_stem_idx)
            if god_stem_5e and 'e' in god_stem_5e:
                elements[god_stem_5e['e']] += 1
            
            # Add hidden gods' elements from the earth branch
            earth_idx = pillar_data['e']
            hidden_gods = findEarthstemGod(earth_idx)
            for _, god_idx in hidden_gods.items():
                hidden_god_5e = findGodstem5E(god_idx)
                if hidden_god_5e and 'e' in hidden_god_5e:
                    elements[hidden_god_5e['e']] += 1
    
    return elements

def calcYellowBlackPath(year, month, day, hour=None, minute=None, second=None, bazi_data=None):
    """
    Calculate the yellow/black path god (黄黑道) for a given date or bazi.
    
    The yellow/black path is determined by the month's earth branch and the day's earth branch.
    There are 12 gods in a specific order, and different month earth branches use different
    sequences of earth branches to map to these gods.
    
    Args:
        year (int): Year
        month (int): Month (1-12)
        day (int): Day
        hour (int, optional): Hour (0-23)
        minute (int, optional): Minute (0-59)
        second (int, optional): Second (0-59)
        bazi_data (dict, optional): Pre-calculated bazi data with y/m/d pillars
        
    Returns:
        dict: Dictionary containing:
            - 'god': The yellow/black path god name
            - 'position': Position in the sequence (1-12)
            - 'month_earth': Month earth branch index (0-11)
            - 'day_earth': Day earth branch index (0-11)
            - 'month_earth_name': Month earth branch name
            - 'day_earth_name': Day earth branch name
            Returns None if calculation fails.
    
    Example:
    >>> calcYellowBlackPath(2023, 8, 15)
    {'god': '青龙', 'position': 1, 'month_earth': 6, 'day_earth': 8, 'month_earth_name': '午', 'day_earth_name': '申'}
    """
    # Use the global Yellow/Black gods dictionary (黄黑道十二神)
    yellow_black_gods = list(gYellowBlackPath.values())
    
    # Earth branch sequences for different months
    # Each sequence maps the 12 earth branches to the 12 gods in order
    month_sequences = {
        # 子/午月: 申酉戌亥子丑寅卯辰巳午未
        0: [8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7],   # 子月
        6: [8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7],   # 午月
        
        # 卯/酉月: 寅卯辰巳午未申酉戌亥子丑  
        3: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1],   # 卯月
        9: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1],   # 酉月
        
        # 寅/申月: 子丑寅卯辰巳午未申酉戌亥
        2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],   # 寅月
        8: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],   # 申月
        
        # 巳/亥月: 午未申酉戌亥子丑寅卯辰巳
        5: [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5],   # 巳月
        11: [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5],  # 亥月
        
        # 辰/戌月: 辰巳午未申酉戌亥子丑寅卯
        4: [4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3],   # 辰月
        10: [4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3],  # 戌月
        
        # 丑/未月: 戌亥子丑寅卯辰巳午未申酉
        1: [10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],   # 丑月
        7: [10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]    # 未月
    }
    
    # Get bazi data if not provided
    if bazi_data is None:
        bazi_data = calcBazi(year, month, day, hour, minute, second)
    
    # Validate that we have the required pillars
    if not bazi_data or 'month' not in bazi_data or 'day' not in bazi_data:
        return None
    
    # Get month and day earth branches
    month_earth = bazi_data['month']['e']
    day_earth = bazi_data['day']['e']
    
    # Get the sequence for this month
    if month_earth not in month_sequences:
        return None
    
    sequence = month_sequences[month_earth]
    
    # Find the position of day earth in the sequence
    try:
        position = sequence.index(day_earth)
        god = yellow_black_gods[position]
        
        return {
            'god': god,
            'position': position + 1,  # 1-based position
            'type': isYellowBlackPathGood(god),  # 'good' or 'bad'
            'month_earth': month_earth,
            'day_earth': day_earth,
            'month_earth_name': gEarthstem[month_earth],
            'day_earth_name': gEarthstem[day_earth]
        }
    except ValueError:
        # This shouldn't happen if the logic is correct
        return None

def getYellowBlackPathGod(index):
    """
    Get the yellow/black path god name by index.
    
    Args:
        index (int): Index (0-11) of the yellow/black path god
        
    Returns:
        str: The god name, or None if index is invalid
        
    Example:
    >>> getYellowBlackPathGod(0)
    '青龙'
    >>> getYellowBlackPathGod(11)
    '勾陈'
    >>> getYellowBlackPathGod(12)
    None
    """
    return gYellowBlackPath.get(index)

def isYellowBlackPathGood(god_name):
    """
    Determine if a yellow/black path god is good (黄道) or bad (黑道).
    
    Args:
        god_name (str): The name of the yellow/black path god
        
    Returns:
        str: 'good' for good gods (黄道), 'bad' for bad gods (黑道), or None if invalid
        
    Example:
    >>> isYellowBlackPathGood('青龙')
    'good'
    >>> isYellowBlackPathGood('天刑')
    'bad'
    >>> isYellowBlackPathGood('invalid')
    None
    """
    return gYellowBlackPathGoodBad.get(god_name)