from datetime import datetime
import math
import re
import warnings
import pandas as pd

from .util import (
    check_datetime,
    get_currency_str,
    get_datetime,
    str_compare,
    str_to_regex,
    str_unit_to_number_unit
)


def get_label_list(relationship_set, concepts, relationship=None):
    """ XBRL의 label list를 변환하는 함수 """
    if relationship is None:
        if len(relationship_set.modelRelationships) > 0:
            preferred = relationship_set.modelRelationships[0].preferredLabel
        else:
            preferred = None
    else:
        preferred = relationship.preferredLabel

    label_ko = concepts.label(lang='ko') if preferred is None \
        else concepts.label(preferredLabel=preferred, lang='ko')
    label_en = concepts.label(lang='en') if preferred is None \
        else concepts.label(preferredLabel=preferred, lang='en')

    res = {
        'concept_id': concepts.id,
        'order': 1.0 if relationship is None else relationship.order,
        'label_ko': label_ko,
        'label_en': label_en,
        'isAbstract': concepts.isAbstract,
        'children': []
    }

    new_relationship_set = relationship_set.fromModelObject(concepts)
    if len(new_relationship_set) > 0:
        new_relationship_set.sort(key=lambda x: x.order)
        for rel in new_relationship_set:
            new_concepts = rel.viewConcept
            res['children'].append(get_label_list(relationship_set, new_concepts, relationship=rel))
    return res


def flatten(tuple_list, res=None):
    """ 2차원 list를 1차원 리스트로 변환하는 함수 """
    results = list() if res is None else res
    for data in tuple_list:
        if isinstance(data, list):
            flatten(data, results)
        else:
            results.append(data)
    return results


def get_max_depth(labels, show_abstract=False):
    """ class의 최대 깊이를 확인하는 함수"""
    max_depth = 0

    if len(labels['children']) == 0 and labels['isAbstract'] is True:
        if show_abstract is True:
            return 1
        else:
            return 0

    for label in labels['children']:
        depth = get_max_depth(label, show_abstract=show_abstract)
        if max_depth < depth:
            max_depth = depth

    return 1 + max_depth


def get_title(cls, lang='ko'):
    """ cls에서 column의 title을 생성하는 함수 """
    title = []

    if cls['instant_datetime'] is None:
        start_date = cls['start_datetime'].strftime('%Y%m%d')
        end_date = cls['end_datetime'].strftime('%Y%m%d')
        title.append('{}-{}'.format(start_date, end_date))
    else:
        instant_date = cls['instant_datetime'].strftime('%Y%m%d')
        title.append('{}'.format(instant_date))
    qlist = []
    for qname in cls['label']:
        qlist.append(cls['label'][qname][lang].strip())
    title.append(tuple(qlist))
    return tuple(title)


def get_datetime_and_name(title):
    """ Column Title에서 날짜와 Tilte 명을 추출하는 함수"""
    regx = re.search(r'\[(.*?)\]', title, re.IGNORECASE)
    result = None
    if regx is not None:
        result = dict()
        instant_datetime = None
        start_datetime = None
        end_datetime = None

        datetime = regx.group(1).split(',')
        if len(datetime) == 1:
            instant_datetime = get_datetime(datetime[0])
        elif len(datetime) == 2:
            start_datetime = get_datetime(datetime[0])
            end_datetime = get_datetime(datetime[1])
        else:
            raise ValueError('Invalid date')

        name = re.sub(r'\[{}\]'.format(regx.group(1)), '', title)
        name = name.strip()

        result['instant_datetime'] = instant_datetime
        result['instant_date'] = start_datetime
        result['start_date'] = end_datetime
        result['name'] = name
    return result


def get_value_from_dataset(classification, dataset, concept_id, label_ko=None):
    """ dataset에서 값을 추출하는 함수 """
    def str_to_float(val):
        try:
            return float(val)
        except ValueError:
            return val

    if isinstance(classification, dict):
        classification = [classification]

    # XBRL 내부 주당이익에서 발생하는 오류 수정을 위한 코드
    currency_unit = None
    if label_ko is not None:
        regex = re.compile(r'\(단위:(.*)\)')
        unit = regex.search(label_ko)
        if unit is not None:
            unit = unit.group(0)
            currency = get_currency_str(unit)
            if currency is not None:
                currency_unit = str_unit_to_number_unit(currency)

    results = list()
    added_title = list()
    for cls in classification:
        value = float('nan')
        for data in dataset[cls['cls_id']]:
            if str_compare(data.concept.id, concept_id):
                value = str_to_float(data.value)
                # XBRL 내부 주당이익에서 발생하는 오류 수정을 위한 코드
                if currency_unit is not None:
                    decimals = str_to_float(data.decimals)
                    # decimals이 없을 경우 0으로 처리
                    if math.isinf(decimals) or math.isnan(decimals):
                        decimals = 0
                    value = value * pow(10, decimals)
                    value = value * currency_unit
                break

        title = get_title(cls, 'en')
        if title in added_title:
            index = added_title.index(title)
            if not math.isnan(value):
                results[index] = value
        else:
            results.append(value)
            added_title.append(title)
    return results


def generate_df_columns(definition, classification, max_depth, lang='ko', show_concept=True, show_class=True):
    """ Table의 DataFrame 변환시 Column Title 생성을 위한 함수"""
    columns = [(definition, 'concept_id')] if show_concept else []
    columns += [(definition, 'label_ko'), (definition, 'label_en')]

    if show_class:
        for idx in range(max_depth):
            columns.append((definition, 'class{}'.format(idx)))

    if isinstance(classification, dict):
        classification = [classification]

    added_title = list()
    for cls in classification:
        title = get_title(cls, lang)
        key = get_title(cls, 'en')
        if key not in added_title:
            columns.append(title)
            added_title.append(key)
    return pd.MultiIndex.from_tuples(columns)


def generate_df_rows(labels, classification, dataset, max_depth,
                     lang="ko", parent=(), show_abstract=False,
                     show_concept=True, show_class=True):
    """ Table의 DataFrame으로 변환시 DataFrame의 Row 생성을 위한 함수"""
    lang_type = {
        'ko': 'label_ko',
        'en': 'label_en',
        'concepts': 'concept_id'
    }
    results = []
    new_parent = list(parent)
    new_parent.append(labels[lang_type[lang]])

    if show_class:
        if len(new_parent) > max_depth:
            return None

    row = None
    if not labels['isAbstract'] or show_abstract:
        row = [labels['concept_id']] if show_concept else []
        row += [labels['label_ko'], labels['label_en']]
        if show_class:
            for idx in range(max_depth):
                if idx < len(new_parent):
                    row.append(new_parent[idx])
                else:
                    row.append(None)
        row.extend(get_value_from_dataset(
            classification,
            dataset,
            labels['concept_id'],
            labels['label_ko']
        ))
        results.append(tuple(row))

    if len(labels['children']) > 0:
        for child in labels['children']:
            generated_row = generate_df_rows(
                child, classification, dataset, max_depth,
                lang=lang, parent=tuple(new_parent), show_abstract=show_abstract,
                show_concept=show_concept, show_class=show_class
            )
            if generated_row is not None:
                results.append(generated_row)
    else:
        return tuple(row) if row is not None else None

    return results


def consolidated_code_to_role_number(code, separate=False):
    """ 코드번호를 Role 번호로 변환하는 함수

    Parameters
    ----------
    code: str
        코드번호
    separate: bool, optional
        개별재무제표 여부

    Returns
    -------
    list of str
        Role 번호 리스트
    """
    consolidated_code = {
        'D1001': ['D210000'],
        'D1002': ['D220000'],
        'D2001': ['D431410'],
        'D2002': ['D431420'],
        'D2003': ['D432410'],
        'D2004': ['D432420'],
        'D2005': ['D310000', 'D410000'],
        'D2006': ['D310000', 'D420000'],
        'D2007': ['D320000', 'D410000'],
        'D2008': ['D320000', 'D420000'],
        'D2009': ['D310000'],
        'D2010': ['D320000'],
        'D3001': ['D610000'],
        'D4001': ['D510000'],
        'D4002': ['D520000'],
    }
    separated_code = {
        'D1001': ['D210005'],
        'D1002': ['D220005'],
        'D2001': ['D431415'],
        'D2002': ['D431425'],
        'D2003': ['D432415'],
        'D2004': ['D432425'],
        'D2005': ['D310005', 'D410005'],
        'D2006': ['D310005', 'D420005'],
        'D2007': ['D320005', 'D410005'],
        'D2008': ['D320005', 'D420005'],
        'D2009': ['D310005'],
        'D2010': ['D320005'],
        'D3001': ['D610005'],
        'D4001': ['D510005'],
        'D4002': ['D520005'],
    }
    return separated_code[code] if separate else consolidated_code[code]


def cls_datetime_check(cls, start_dt, end_dt):
    """ classification 의 시간이 시작일자와 종료일자 사이인지 체크하는 함수

    Parameters
    ----------
    cls: cls
        classification
    start_dt: str
        검색 시작 일자
    end_dt: str
        검색 종료 일자

    Returns
    -------
    bool
        시작일자와 종료일자 사이에 있을시 True / 아닐시 False

    """
    res = True
    instant_datetime = cls.get('instant_datetime')
    if instant_datetime:
        res = res and check_datetime(instant_datetime, start_dt, end_dt)
    start_datetime = cls.get('start_datetime')
    if start_datetime:
        res = res and check_datetime(start_datetime, start_dt, end_dt)
    end_datetime = cls.get('end_datetime')
    if end_datetime:
        res = res and check_datetime(end_datetime, start_dt, end_dt)
    return res


def cls_label_check(cls, query):
    """ classification label에 특정 단어가 포함된지 검색하는 함수

    Parameters
    ----------
    cls: cls
        classification
    query: str
        검색어

    Returns
    -------
    bool
        질의내용 포함시 True / 미포함시 False
    """
    if query is None:
        return True
    regex = str_to_regex(query)
    label = ''
    for qname in cls['label']:
        label = label + cls['label'][qname]['ko'] + cls['label'][qname]['en']
    if regex.search(label):
        return True
    return False


def cls_merge_type(classification):
    """ classification type이 2가지일 때 합쳐주는 함수

    Parameters
    ----------
    classification: cls
        classification 리스트

    Returns
    -------
    list of cls
        변환된 classification 리스트
    """

    cls_datetime = {}

    for cls in classification:
        end_datetime = cls.get('end_datetime')
        start_datetime = cls.get('start_datetime')
        if end_datetime is not None:
            cls_datetime[end_datetime] = start_datetime

    if len(cls_datetime) > 0:
        for cls in classification:
            instant_datetime = cls.get('instant_datetime')
            if instant_datetime:
                start_datetime = cls_datetime.get(instant_datetime)
                if start_datetime is None:
                    warnings_text = "start_datetime could not be found. " \
                                    "Therefore, start_datetime is set to January 01, 1900"
                    warnings.warn(warnings_text, RuntimeWarning)
                    start_datetime = datetime(year=1900, month=1, day=1)
                cls['instant_datetime'] = None
                cls['start_datetime'] = start_datetime

                cls['end_datetime'] = instant_datetime

    return classification
