from   logging             import getLogger, config, DEBUG, INFO, WARN, ERROR, CRITICAL
import json
import os
import sys
import codecs as cd
import math
import locale
import datetime
import csvExcelConst
import unicodedata
import datetime
import smtplib
import base64
import ssl
from   email                    import encoders
from   email.mime.base          import MIMEBase
from   email.mime.multipart     import MIMEMultipart
from   email.mime.text          import MIMEText
from   email.header             import Header
from   email                    import charset
from   email.utils              import formatdate
import configparser
import errno

from   csvExcelImportXml        import csv_excel_import_xml 
import pandas as pd 
import openpyxl
from   openpyxl.styles.fonts    import Font
from   openpyxl.worksheet.table import Table, TableStyleInfo

    
class send_mail_data:

        def __init__(self, config_mail:[str]):

            #! send_mail(config_mail:[str], from_addr, to_addr:[str], cc_addr:[str] = None, bcc_addr:[str] = None,
            #!           subject:str, body_text:str, attach_file:[str]):

            self.__config_mail      = config_mail
            self.__from_addr        = ""
            self.__to_addr          = []
            self.__cc_addr          = []
            self.__bcc_addr         = []
            self.__request_time     = ""
            self.__subject          = ""
            self.__body             = ""
            self.__attach_file      = []
            
        @property
        def config_mail(self) -> {}:
            return self.__config_mail
        
        @config_mail.setter 
        def config_mail(self, config_mail:{}) -> None:
            self.__config_mail = config_mail

        @property
        def from_addr(self) -> str:
            return self.__from_addr
        
        @from_addr.setter
        def from_addr(self, from_addr:str) -> None:
            self.__from_addr = from_addr
        
        @property
        def to_addr(self) -> [str]:
            return self.__to_addr
        
        @to_addr.setter
        def to_addr(self, to_addr:[str]) -> None:
            self.__to_addr = to_addr
        
        @property
        def cc_addr(self) -> [str]:
            return self.__cc_addr
        
        @cc_addr.setter
        def cc_addr(self, cc_addr:[]) -> None:
            self.__cc_addr = cc_addr
    
        @property
        def bcc_addr(self) -> [str]:
            return self.__bcc_addr
        
        @bcc_addr.setter
        def bcc_addr(self, bcc_addr:[str]) -> None:
            self.__bcc_addr = bcc_addr
        
        @property
        def request_time(self) -> str:
            return self.__request_time

        @request_time.setter
        def request_time(self, request_time) -> None:
            self.__request_time = request_time

        @property
        def subject(self) -> str:
            return self.__subject
        
        @subject.setter
        def subject(self, subject:str) -> None:
            self.__subject = subject

        @property
        def body(self) -> str:
            return self.__body
        
        @body.setter
        def body(self, body_txt) -> None:
            self.__body = body_txt
        
        @property
        def attachment_file(self) -> [str]:
            return self.__attach_file
        
        @attachment_file.setter
        def attachment_file(self, attactment_file) -> None:
            self.__attach_file = attactment_file


class csv_excel_util:

    def __init__(self, log):
        
        # =================================
        # 構成ファイルの読込み
        # ---------------------------------
        execute_dir = os.path.dirname(__file__)
        config_ini      = configparser.ConfigParser()
        config_ini_path = f"{execute_dir}/config.ini"
        if  not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini)
    
        config_ini.read(config_ini_path, encoding='utf-8')
        self.__log = log

    @property
    def log(self):
        return self.__log
    
    # --------------------------------------------------------
    # フォルダー、ファイル名、拡張子の取得
    # --------------------------------------------------------
    def get_parse_file_name(self, file_name) -> (str,str,str):
        base_path = os.path.dirname(file_name)       # フォルダー名のみを取得
        base_file = os.path.basename(file_name)      # ファイル名のみ（拡張子含む）
        base_name = os.path.splitext(base_file)[0]   # ファイル名のみ（拡張子なし）
        base_ext  = os.path.splitext(base_file)[1]   # ファイル名の拡張子のみ

        return base_path, base_name, base_ext

    # ---------------------------------------------------------------
    # ＣＳＶファイルを読込む
    # ---------------------------------------------------------------
    def load_csv_file(self, csv_file_name, encoder_str:str = 'cp932'):

        with cd.open(csv_file_name, "r", encoder_str, "ignore") as csv_file:  # ignoreで文字コード変換エラーを無視する（※外字を使用する為）
            csv_data = pd.read_csv(csv_file)

        return csv_data

    # --------------------------------------------------------
    # 空のＥＸＣＥＬファイルを生成
    # --------------------------------------------------------
    def create_empty_excel_file(self, excel_file_name) -> None:
        workbook = openpyxl.Workbook()  #自動的にシート名「sheet」が0番目に作成される。
        workbook.save(excel_file_name)
        # workbook.close
        # return

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルを読込む
    # --------------------------------------------------------
    def load_excel_file(self, excel_file_name) -> openpyxl.workbook.Workbook:
        wb = openpyxl.load_workbook(excel_file_name)
        return wb 

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルのシート名一覧を取得する
    # --------------------------------------------------------
    def get_sheet_names(self, workbook) -> (str,int):
        
        sheet_names = workbook.getSheetNames
        sheet_count = len(sheet_names)
        return sheet_names, sheet_count     

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルのアクティブシートを取得する
    # --------------------------------------------------------
    def get_active_sheet(self, workbook) -> openpyxl.worksheet.worksheet:
        
        sheet = workbook.active
        return sheet     

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルのアクティブシートの変更
    # --------------------------------------------------------
    def change_active_sheet(self, workbook, sheet_name) -> None:
        
        for ws in workbook.worksheets:
            if ws.title.casefold() == sheet_name.casefold():
                ws.sheet_view.tabSelected = True
                return

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルのシート名を変更する
    # --------------------------------------------------------
    def rename_sheet_name(self, sheet, new_name) -> None :
        
        sheet.title = new_name
    
    # --------------------------------------------------------
    # ＥＸＣＥＬファイルのシート名が存在しているかチェックする
    # --------------------------------------------------------
    def check_sheet_names(self, workbook, sheet_name) -> bool:
        
        sheet_names = workbook.getSheetNames
        
        for ck_name in sheet_names:
            if ck_name.casefold() == sheet_name.casefold():
                return True
        
        return False

    # --------------------------------------------------------
    # ＥＸＣＥＬファイル最後にシートを追加する
    # --------------------------------------------------------
    def add_sheet_name(self, workbook, sheet_name) -> openpyxl.worksheet.worksheet:
        
        workbook.create_sheet(title=sheet_name, index=len(workbook.sheetNames))

        sheet = workbook[sheet_name]
        return sheet

    # --------------------------------------------------------
    # ＥＸＣＥＬファイルの指定したシートを削除する
    # --------------------------------------------------------
    def delete_sheet_name(self, workbook, sheet_name) -> None:
        
        for ws in workbook.worksheets:

            if ws.title.casefold() == sheet_name.casefold():
                workbook.remove(ws)
                return

    # --------------------------------------------------------
    # ＥＸＣＥＬの列文字の取得
    # --------------------------------------------------------
    def get_excel_col_name(self, cols) -> str:

        col_suffix     = ""
        col_suffix_num = math.floor((cols - 1) / 26)
        if col_suffix_num > 0:
            col_suffix = chr(0x0040 + col_suffix_num)
        
        col_mod = cols % 26
        if col_mod == 0:
            col_str = 'Z'
        else:
            col_str = chr(0x0040 + (cols % 26))

        return col_suffix + col_str
    # --------------------------------------------------------
    # ＥＸＣＥＬのフォント及びフォントサイズ設定
    # --------------------------------------------------------
    def set_excel_font(self, ws, font_name, font_size) -> None:
        
        self.log.log(DEBUG, f"フォント「{font_name}」、フォントサイズ「{font_size}」")
        font = Font(name=font_name, size=font_size)
        for row in ws:

            for cell in row:
                ws[cell.coordinate].font = font

                #  print(cell.coordinate) # 現在のセルの位置　例：A4
    # --------------------------------------------------------
    # ＥＸＣＥＬの列幅設定
    # --------------------------------------------------------
    def set_excel_cell_width(self, ws) -> None:

        for col in ws.columns:
            max_length = 0
            column = col[0].column

            for cell in col:
                
                tmp_len = 0
                tmp_str = str(cell.value).strip()
                tmp_max = len(tmp_str)
                if tmp_max > 0:
                    for i in range(0,len(tmp_str)):
                        tmp_char = tmp_str[i:i+1]
                        if unicodedata.east_asian_width(tmp_char) in 'FWA':# 半角文字は１バイト、全角は２バイトで文字数を数える
                            tmp_len += 2
                        else:
                            tmp_len += 1
                        #! work_len = len(tmp_char.encode('utf-8'))
                        #! if work_len >= 3: # 漢字の場合は、３バイト以上になるが、列幅換算は２バイト
                        #!     tmp_len += 2
                        #!  else:
                        #!      tmp_len += 1  # 半角文字は１バイト
                    if  tmp_len > max_length:
                        max_length = tmp_len

            adjusted_width = (max_length + 2) * 1.2
            column_name = self.get_excel_col_name(column)
            ws.column_dimensions[column_name].width = adjusted_width
            #! self.log.log(DEBUG, f"列名＝{column_name}, 列幅＝" + str(adjusted_width))    

    # --------------------------------------------------------
    # ＥＸＣＥＬの表示色を英語に変換
    # --------------------------------------------------------
    def get_convert_color_name(self, color_name) -> str:
        
        # ======( openpyxlでは表示形式で表示色の日本語がサポートされないので、日本語の色を英字の色に変換開始)======
        color_name = color_name.replace('赤', 'Red')
        color_name = color_name.replace('黒', 'Black')
        color_name = color_name.replace('青', 'Blue')
        color_name = color_name.replace('水', 'Cyan')
        color_name = color_name.replace('緑', 'Green')
        color_name = color_name.replace('紫', 'Magenta')
        color_name = color_name.replace('白', 'White')
        color_name = color_name.replace('黄', 'Yellow')
        # ======( openpyxlでは表示形式で表示色の日本語がサポートされないので、日本語の色を英字の色に変換終了)======
            
        return color_name
    # --------------------------------------------------------
    # ＥＸＣＥＬの表示形式設定（ピボットテーブルに設定）
    # --------------------------------------------------------
    def set_excel_cell_edit_format2(self, ws, edit_format:str, start_row:int, start_col:int) -> None:

        max_cols = ws.max_column
        max_rows = ws.max_row

        self.log.log(DEBUG, f"max_row = {max_rows}, max_cols = {max_cols}")
        edt_fmt  = self.get_convert_color_name(edit_format)
        # 行単位のループ
        for row in range(start_row, max_rows):
            # 列単位のループ
            for col in range(start_col, max_cols):
                col_str  = self.get_excel_col_name(col)
                cell_str = col_str + str(row)

                ws[cell_str].number_format = edt_fmt

    # --------------------------------------------------------
    # ＥＸＣＥＬの表示形式設定（定義情報より）
    # --------------------------------------------------------
    def set_excel_cell_edit_format(self, ws, def_item:dict, num_rows:int) -> None:
        
        dict_members = def_item[csvExcelConst.COLS]

        for mem in dict_members:
            mem2:dict = dict_members[mem]

            col_number_num  = int(mem2[csvExcelConst.COL_NO])
            col_number_char = self.get_excel_col_name(col_number_num)   # カラム番号からカラム文字を取得する
            
            col_edit_format:str = mem2[csvExcelConst.EDIT_FORMAT]
            col_edit_format     = self.get_convert_color_name(col_edit_format)
            #! num_rows   = def_item[csvExcelConst.BND_CSV_ROWS] 
            edit_number_rows:int = num_rows + 2  # タイトル行の分＋１とrangeは１つ前で終了するので＋１、合計＋２
        
            for i in range(2, edit_number_rows) :
                cell_char   = col_number_char + str(i)
                ws[cell_char].number_format = col_edit_format
                # log.log(DEBUG, f"列No＝%s 表示形式＝%s" % (cell_char, col_edit_format))

    # --------------------------------------------------------
    # ＥＸＣＥＬにテーブル設定
    # --------------------------------------------------------
    def set_excel_table(self, bnd_name, ws, def_head:dict, num_rows:int, num_cols:int) -> None:

        cell_range="A1:" + self.get_excel_col_name(num_cols) + str(num_rows + 1)    # +1はヘッダー行
        table_name  = def_head[csvExcelConst.TABLE_NAME] + bnd_name                 # １つのワークブックでは同じテーブル名を使用出来ないので連番を付加
        table_style = def_head[csvExcelConst.TABLE_STYLE]

        self.log.log(DEBUG, f"bnd={bnd_name}、tableName={table_name}、tableStyle={table_style}、Range={cell_range}")

        table_style = TableStyleInfo(name=table_style, showRowStripes=True)
        table = Table(displayName=table_name, ref=cell_range)
        table.tableStyleInfo = table_style

        ws.add_table(table)    

    # --------------------------------------------------------
    # タイムスタンプ文字を取得する
    # --------------------------------------------------------
    def get_timestamp_string(self) -> str:

        to_day_time     = datetime.datetime.now()                  # 現在の日付・時刻を取得
        to_day_time_str = to_day_time.strftime('%Y_%m_%d_%H_%M_%S')  # 現在の日付・時刻を文字に変換

        return to_day_time_str

    # -----------------------------------------------------------------------
    # bndファイル及びbndに定義されているＣＳＶ、ＤＥＦファイルの存在チェック
    # -----------------------------------------------------------------------
    def check_bnd_files(self, bnd_file_name:str) -> (bool,dict,str):

        rtn_flag    = True
        dict_bnd    = {}

        if  not os.path.isfile(bnd_file_name):
            err_msg = f"BNDファイルが見つかりません。ファイル名＝{bnd_file_name}"
            self.log.log(CRITICAL, err_msg)
            return True, dict_bnd, err_msg

        csv_imp  = csv_excel_import_xml(self.log)
        dict_bnd = csv_imp.get_bnd(bnd_file_name)
        #! output_base_file_name = dict_bnd[csvExcelConst.BND_OUTPUT_FILE]

        for bnd in dict_bnd[csvExcelConst.BND_BIND]:
            dict_mem = dict_bnd[csvExcelConst.BND_BIND].get(bnd)

            bnd_csv   = dict_mem[csvExcelConst.BND_CSV_FILE]
            #! bnd_sheet = dict_mem[csvExcelConst.BND_SHEET_NAME]

            base_path, base_name, _base_ext = self.get_parse_file_name(bnd_csv)
            bnd_def   = os.path.join(base_path, base_name) + ".DEF"

            dict_mem[csvExcelConst.BND_DEF_FILE] = bnd_def      # 生成したＤＥＦファイル名を保存する

            self.log.log(DEBUG, f"No = {bnd}, ＣＳＶ＝{bnd_csv}、ＤＥＦ＝{bnd_def}")
            rtn_flag, err_msg = self.check_csv_def_files(bnd_csv, bnd_def)
            if  rtn_flag == False:
                return rtn_flag, dict_bnd, err_msg

            self.log.log(INFO, f"ＳＥＱ＝{bnd}、CSVファイル名＝{bnd_csv}、defファイル名＝{bnd_def}")

        #log.log(DEBUG, dict_bnd)
        return rtn_flag, dict_bnd, ""

    # -----------------------------------------------------------------------
    # ＣＳＶ、ＤＥＦファイルの存在チェック
    # -----------------------------------------------------------------------
    def check_csv_def_files(self, csv_file_name, def_file_name) -> (bool,str):

        rtn_flag:bool = True
        err_msg:str   = ""
        if  not os.path.isfile(csv_file_name):
            err_msg = f"ＣＳＶファイルが見つかりません。ファイル名＝{csv_file_name}"
            self.log.log(CRITICAL, err_msg)
            return False, err_msg

        if  not os.path.isfile(def_file_name):
            err_msg = f"ＤＥＦファイルが見つかりません。ファイル名＝{def_file_name}"
            self.log.log(CRITICAL, err_msg)
            return False, err_msg

        return rtn_flag, ""

    # -----------------------------------------------------------------------
    # ＣＳＶファイル名からＤＥＦファイル名を取得する
    # -----------------------------------------------------------------------
    def get_def_file_name(self, csv_file_name) -> str:

        base_path, base_name, _base_ext = self.get_parse_file_name(csv_file_name)
        def_file_name = os.path.join(base_path, base_name) + ".DEF"

        return def_file_name

    # ---------------------------------------------------------------------------------
    # ＥＸＣＥＬに設定するシート名は最大３１文字。引数の文字列の最大３０文字を取得する
    # ---------------------------------------------------------------------------------
    def get_sheet_name_with_maxlen(self, sheet_name) -> str:

        return sheet_name[:30]

    # ---------------------------------------------------------------
    # メール送信  ※メール送信時エラーは上記でTry-Catchする事
    # ---------------------------------------------------------------
    def send_mail(self, mail_data: send_mail_data):

        mail_send:str           = mail_data.config_mail['MailSend']

        if  mail_send.casefold() != 'True'.casefold():
            return
        
        mail_server:str         = mail_data.config_mail['MailServer']
        mail_portno:str         = mail_data.config_mail['MailPort']
        mail_tls:str            = mail_data.config_mail['TLS']
        mail_account_id:str     = mail_data.config_mail['MailUserId']
        mail_account_pass:str   = mail_data.config_mail['MailUserPass']
        mail_charset:str        = mail_data.config_mail['CharSet']
        mail_debug_level:int    = int(mail_data.config_mail['DEBUG_LEVEL'])

        mail_from_addr:str      = mail_data.config_mail['FromAddr']

        mail_to_list:str        = ""
        mail_cc_list:str        = ""
        mail_bcc_list:str       = ""
        
        if  mail_data.cc_addr is not None:
            mail_to_list        = ", ".join(mail_data.to_addr)        # 配列をカンマ区切りの文字にする
    
        if  mail_data.cc_addr is not None:
            mail_cc_list        = ", ".join(mail_data.cc_addr)        # 配列をカンマ区切りの文字にする

        if  mail_data.bcc_addr is not None:
            mail_bcc_list       = ", ".join(mail_data.bcc_addr)       # 配列をカンマ区切りの文字にする

        if  mail_charset is None or mail_charset == "":
            mail_charset = "ISO-2022-JP"

        mail_subject:str    =   mail_data.subject
        mail_body:str       =   mail_data.body
        
        msg  = MIMEMultipart()                                  # multipart/mixed; MIME-Version="1.0" のMIME情報が追加される
        msg['Subject']  = Header(mail_subject, mail_charset)    # SubjectはISO-2022-JPでエンコードさせる（※日本語メールを送る為の仕様）
        msg['From']     = mail_from_addr                        # 必須パラメータなので必ず値はセットされている
        
        # 下記はToやCcやBccに空文字をセットするとRCPTコマンドが発行されるので、有効なTo及びCc及びBccでない限りセットしない
        if  mail_to_list != "":
            msg['To']       = mail_to_list                  

        if  mail_cc_list != "":
            msg['Cc']       = mail_cc_list

        if  mail_bcc_list != "":
            msg['Bcc']      = mail_bcc_list
        
        msg['Date']     = formatdate(localtime=True)
        msg.set_default_type('text/plan')
        
        #==============================================================================================================================
        # メール本文の追加（本文は文字コードISO2022-JPにしBase64エンコード、MIMEタイプ text/plainで文字コードをiso-2022-jpにセットする
        #------------------------------------------------------------------------------------------------------------------------------
        body = MIMEText(base64.b64encode(mail_body.encode(mail_charset, "ignore")), "plain", mail_charset,)     # ここではContent-Transfer-Encoding: 7bitとなる
        body.replace_header('Content-Transfer-Encoding', "base64")                                              # base64エンコードしているのでContent-Transfer-Encodingをbase64に置換える
        msg.attach(body)
        
        #===============================
        # ファイルを添付
        #-------------------------------
        for file in mail_data.attachment_file:
            fname = os.path.basename(file)
            part  = MIMEBase('application', 'octet-stream')   # アプリケーションタイプはoctet-streamとする（ダウンロードになる）
                                                              # ↑本来は、application/vnd.ms-excel が最適
            part.set_payload(open(file, 'rb').read())
            encoders.encode_base64(part)                      # partの中身をBase64エンコードする
            part.add_header('Content-Disposition', 'attachment', fileName=fname)
            msg.attach(part)

        # ===============================
        # メールサーバーへメールを送信
        # -------------------------------
        if  mail_tls.casefold() == "no":
            server = smtplib.SMTP(host=mail_server, port=int(mail_portno))
        else:
            server = smtplib.SMTP_SSL(host=mail_server, port=int(mail_portno))

        self.log.log(DEBUG, f"Mail Debug Level = {mail_debug_level}")
        if  mail_debug_level > 0:
            server.set_debuglevel(mail_debug_level)       # DEBUG USE
        
        server.login(mail_account_id, mail_account_pass)  # メールサーバーがサポートしている認証を元に、 提供している認証メカニズム(auth【AUTH コマンド送信】 参照)でログインする
        errors = server.send_message(msg)
        if  isinstance(errors, dict) and len(errors) > 0:
            errmsg = f'Failed to send Details: {errors}'
            server.quit()
            return errmsg, False

        server.quit()
        
        return "", True

