from sdsvkie.utils.post_processing import normalize_number_wordgroup
from sdsvkie.utils import Word_group
from dateutil import parser
from rapidfuzz.distance import Levenshtein
from sdsvkie.utils.post_processing.common_post_processing import normalize_number
import re
import math 
from sdsvkie.utils.word_formation import merge_boxes

RATIO_VALID_DISTANCE = 3
SIMILAR_SCORE_THR = 0.7
GOJEK_KEYWORD = "gojek"


def cal_score_similar(str1, str2):
    return Levenshtein.normalized_similarity(str1, str2)

def is_grab_seller(seller_wordgroups, words=False, thr=0.7):
    SELLER_GRAB_NAMES = [
        "justgrab",
        "grabcar",
        "grabcar premium",
        "standard taxi"
    ]
    for seller_wg in seller_wordgroups:
        for keyword_grab in SELLER_GRAB_NAMES:
            # print(f"{seller_wg.text.lower()} - {keyword_grab} - {cal_score_similar(seller_wg.text.lower(), keyword_grab)}")
            if cal_score_similar(seller_wg.text.lower().strip(), keyword_grab) > thr \
                or "grab" in seller_wg.text.lower().strip():
                return True
    return False



def is_wg_same_line(pre_wg, curr_wg, thresh=0.7):
    def _cal_ratio(top1, bottom1, top2, bottom2):
        sorted_vals = sorted([top1, bottom1, top2, bottom2])
        intersection = sorted_vals[2] - sorted_vals[1]
        min_height = min(bottom1 - top1, bottom2 - top2)
        if min_height == 0:
            return -1
        ratio = intersection / min_height
        return ratio

    def _cal_ratio_height(top1, bottom1, top2, bottom2):

        height1, height2 = top1 - bottom1, top2 - bottom2
        ratio_height = float(max(height1, height2)) / float(min(height1, height2))
        return ratio_height
    # calculate iou in vertical direction
    _, top1, _, bottom1 = pre_wg.boundingbox
    _, top2, _, bottom2 = curr_wg.boundingbox

    ratio = _cal_ratio(top1, bottom1, top2, bottom2)
    ratio_height = _cal_ratio_height(top1, bottom1, top2, bottom2)

    if (
        (top1 in range(top2, bottom2) or top2 in range(top1, bottom1))
        and ratio >= thresh
        and (ratio_height < 2)
    ):
        return True
    return False


def is_gojek_seller(words=None, thr=0.7):
    
    texts = [word.text for word in words]
    texts_str = " ".join(texts)
    if GOJEK_KEYWORD in texts_str:
        return True
    else:
        return False


def strip_wordgroup(wg):
    wg.text = wg.text.strip()
    return wg

def postprocess_seller(seller_wordgroups, words):
    """Seller: maybe have multiple lines
    - if two wordgroup have too far distance, get highest y-axis wordgroup 

    Args:
        seller_wordgroups (_type_): _description_
    """
    # if len(seller_wordgroups) == 1:
    #     return seller_wordgroups

    # remove seller wg that have too far distance from top seller
    valid_seller_wg = []
    seller_wordgroups = sorted(seller_wordgroups, key=lambda wg: wg.boundingbox[1])
    top_seller_wg = seller_wordgroups[0]
    valid_seller_wg.append(top_seller_wg)
    height_top_seller_wg = abs(top_seller_wg.boundingbox[3] - top_seller_wg.boundingbox[1])
    for seller_wg in seller_wordgroups[1:]:
        if y_distance(seller_wg.boundingbox, valid_seller_wg[-1].boundingbox) <= height_top_seller_wg * RATIO_VALID_DISTANCE:
            valid_seller_wg.append(seller_wg)
    # print(f"height_top_seller_wg: {height_top_seller_wg}, height_top_seller_wg * RATIO_VALID_DISTANCE = {height_top_seller_wg * RATIO_VALID_DISTANCE}")
    
    if is_grab_seller(seller_wordgroups, thr=SIMILAR_SCORE_THR):
        valid_seller_wg[0].text = "Grab"
        return [valid_seller_wg[0]]
    elif is_gojek_seller(seller_wordgroups, words):
        valid_seller_wg[0].text = "Gojek"
        return [valid_seller_wg[0]]
    elif len(valid_seller_wg) > 1:

        # Sort seller wordgroups
        valid_seller_wg = sorted(valid_seller_wg, key=lambda wg: wg.boundingbox[1])   # sort by y-axis
        wg_lines = []
        line = [valid_seller_wg[0]]
        for idx, wg in enumerate(valid_seller_wg[1:]):
            if is_wg_same_line(line[-1], wg):
                line.append(wg)
            else:
                line = sorted(line, key=lambda wg: wg.boundingbox[0])   # sort wg by x-axis in a line
                wg_lines.append(line)

                line = [wg]
            if idx == len(valid_seller_wg[1:]) - 1:
                line = sorted(line, key=lambda wg: wg.boundingbox[0])
                wg_lines.append(line)
        valid_seller_wg = [wg for line in wg_lines for wg in line]
    #strip text
    valid_seller_wg = [strip_wordgroup(wg) for wg in valid_seller_wg]

    return valid_seller_wg 


def normalize_id(id_text):
    id_text = id_text.upper()
    
    split_chars = ["#:", ":"]
    for split_char in split_chars:
        if split_char in id_text:
            id_text = id_text.split(split_char)[-1]
        
    keywords = ["NO.","NO:", "NO#", "BILL", "RCDT", "RCPT", "#", "."]
    for keyword in keywords:
        id_text = id_text.replace(keyword, "")

    id_text = re.sub(r"[^0-9A-Z-]", "", id_text)
    return id_text

def postprocess_id(id_wordgroups):
    """Only one wordgroup

    Args:
        id_wordgroups (list[Wordgrou[]]): list of id wordgroups
    """
    id_wordgroups = id_wordgroups[:1]  # only get the first id
    id_wordgroups[0].text = normalize_id(id_wordgroups[0].text)   

    return id_wordgroups


def parse_date(date_str):
    # remove space
    date_str = re.sub(r"/\s+", "/", date_str)
    date_str = re.sub(r"-\s+", "-", date_str)
    is_parser_error = False 
    try:
        date_obj = parser.parse(date_str, fuzzy=True)
        year_str = str(date_obj.year)
        day_str = str(date_obj.day)
        date_formated = date_obj.strftime("%d/%m/%Y")
    except Exception as err:
        
        print(f"Error parse date: err = {err}, date = {date_str}")
        # from IPython import embed; embed()
        date_formated = date_str
        is_parser_error = True 
        return date_formated, is_parser_error
        
    if len(normalize_number(date_str)) == 6:
        year_str = year_str[-2:]
    try:
        year_index = date_str.index(str(year_str))
        day_index = date_str.index(str(day_str))
        if year_index > day_index:
            date_obj = parser.parse(date_str, fuzzy=True, dayfirst=True)

            date_formated = date_obj.strftime("%d/%m/%Y")
    except Exception as err:
        print(f"Error check dayfirst: err = {err}, date = {date_str}")

    return date_formated, is_parser_error

def postprocess_date(date_wordgroups, total_value_wordgroup=None):
    """
    --> parse all date wordgroups to standard format 
    - 1 date -> ok 
    - >= 2 date 
        + if the same date -> get highest y-axis
        + if different date:
            - get highest y-axis 
    """

    date_wordgroups = sorted(date_wordgroups, key=lambda wg: wg.boundingbox[1])
    date_str, status = parse_date(date_wordgroups[0].text)
    date_wordgroups[0].text = date_str
    return date_wordgroups[:1]


# total money utils
def check_same_value(total_wordgroups):
    is_same = True
    prev_value = total_wordgroups[0]
    for total_wg in total_wordgroups[1:]:
        if total_wg.text != prev_value:
            is_same = False 

    return is_same


def find_best_total_key(total_key_wordgroups):
    """_summary_

    Args:
        total_key_wordgroups (_type_): _description_
    """
    if len(total_key_wordgroups) == 1:
        return total_key_wordgroups[0]
    else:
        #priority:
        ## 1. 'total' keyword in key 
        ## 2. 'amount paid' in key
        ## 3. highest y-axis 
        for total_key_wg in total_key_wordgroups:
            if 'sub total' in total_key_wg.text.lower():
                continue
            if 'total' in total_key_wg.text.lower() and 'sub total' not in  total_key_wg.text.lower():
                return total_key_wg
            if 'amount paid' in total_key_wg.text.lower():
                return total_key_wg
            
        total_key_wordgroups = sorted(total_key_wordgroups, key=lambda wg: wg.boundingbox[1])
        best_total_key = total_key_wordgroups[0]

    return best_total_key

def y_distance(box_1, box_2):
    return abs((box_1[1] + box_1[3]) / 2 - (box_2[1] + box_2[3])/ 2)




def get_best_total_value_wordgroup(total_value_wordgroups, total_key_wg):
    total_value_wordgroups = sorted(total_value_wordgroups, key=lambda total_value_wg: y_distance(total_value_wg.boundingbox, total_key_wg.boundingbox)) 
    
    # merge total value if have multi boxes in one line
    line = [total_value_wordgroups[0]]
    for idx, wg in enumerate(total_value_wordgroups[1:]):
        if is_wg_same_line(line[-1], wg):
            line.append(wg)
        
    total_value_line = sorted(line, key=lambda wg: wg.boundingbox[0])   # sort wg by x-axis in a line
    if len(total_value_line) == 1:
        return total_value_line[0]
    
    #text
    total_value_text = ".".join([wg.text for wg in total_value_line if wg.text.strip() != ""])
    total_value_text = total_value_text.replace("..", ".")
    #box
    total_value_box = merge_boxes([wg.boundingbox for wg in total_value_line])
    #label
    total_value_label = total_value_line[0].kie_label
    #conf
    conf_list = [wg.conf for wg in total_value_line]
    total_value_conf = sum(conf_list) / len(conf_list)

    # new value wg
    total_value_wg = Word_group()
    total_value_wg._set_values(
        boundingbox=total_value_box,
        text=total_value_text,
        kie_label=total_value_label,
        conf=total_value_conf
    )


    return total_value_wg


def postprocess_total(total_key_wordgroups, total_value_wordgroups, reserve_dot=False):
    """
    Case 1: 1 total value 
    Case 2: >1 total value 
        - the same value -> get highest y-axis box
        - different value -> get total_key's box that have highest y-axis -> get total_value have the same line

    """
    total_value_wordgroups = [normalize_number_wordgroup(wg, reserve_dot=reserve_dot) for wg in total_value_wordgroups]
    if len(total_value_wordgroups) == 1:
        return total_value_wordgroups
    elif len(total_value_wordgroups) > 1:
        if check_same_value(total_value_wordgroups) or len(total_key_wordgroups) == 0:
            total_value_wordgroups = sorted(total_value_wordgroups, key=lambda wg : wg.boundingbox[1])
            best_total_value_wordgroup = total_value_wordgroups[0]
        
        else:
            # use total_key
            total_key_wg = find_best_total_key(total_key_wordgroups)
            best_total_value_wordgroup = get_best_total_value_wordgroup(total_value_wordgroups, total_key_wg)
    return [best_total_value_wordgroup]

def remove_special_token(wg):
    wg.text = wg.text.replace("✪", " ")
    return wg

def process_remove_special_token(kie_dict):
    new_kie_dict = {}
    for field_key, list_wg in kie_dict.items():
        new_list_wg = [remove_special_token(wg) for wg in list_wg]
        new_kie_dict[field_key] = new_list_wg

    return new_kie_dict

def receipt_postprocess(kie_dict: dict, words=None, return_currency=False):

    ### Remove special token
    kie_dict = process_remove_special_token(kie_dict)

    # ID
    if 'no_value' in kie_dict:
        kie_dict['no_value'] = postprocess_id(kie_dict['no_value'])
    else:
        kie_dict['no_value'] = []
    # Store name
    if 'seller_company_name_value' in kie_dict:
        kie_dict['seller_company_name_value'] = postprocess_seller(kie_dict['seller_company_name_value'], words)
    else:
        kie_dict['seller_company_name_value'] = []
    # Date
    if 'date_value' in kie_dict:
        kie_dict['date_value'] = postprocess_date(kie_dict['date_value'])
    else:
        kie_dict['date_value'] = []
    # Total
    if 'total_value' in kie_dict:

        total_key_wordgroups = [] if 'total_key' not in kie_dict else kie_dict['total_key']
        kie_dict['total_value'] = postprocess_total(
            total_key_wordgroups=total_key_wordgroups, 
            total_value_wordgroups=kie_dict['total_value'],
            reserve_dot=True)
        # print(kie_dict['Total_value'][0].text)
    else:
        kie_dict['total_value'] = []

    # mapping

  
    return kie_dict