# -*- coding: utf-8 -*-
# 项目: jdg-hd-node-farm
# Copyright 2018 JDG <www.yunside.com>
# Created by LLH <lianghua.liu@yunside.com> at 2018/9/24

import logging
import csv
import datetime
import io
import itertools
import psycopg2
import operator
import os
import re
import base64
import ast

_logger = logging.getLogger(__name__)

from odoo import api, models, fields, SUPERUSER_ID
from odoo.exceptions import ValidationError, Warning
from odoo.tools.translate import _
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.misc import ustr
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT

from odoo.osv import expression

from core.middleware import utils

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

try:
    import xlrd

    try:
        from xlrd import xlsx
    except ImportError:
        xlsx = None
except ImportError:
    xlrd = xlsx = None

try:
    import odf_ods_reader
except ImportError:
    odf_ods_reader = None

FILE_TYPE_DICT = {
    'text/csv': ('csv', True, None),
    'application/vnd.ms-excel': ('xls', xlrd, 'xlrd'),
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ('xlsx', xlsx, 'xlrd >= 0.8'),
    'application/vnd.oasis.opendocument.spreadsheet': ('ods', odf_ods_reader, 'odfpy')
}

EXTENSIONS = {
    '.' + ext: handler
    for mime, (ext, handler, req) in FILE_TYPE_DICT.iteritems()
}

# 用于匹配单据类型与对应的执行方法，master下需覆盖赋值
utils.G_PARAMS['data_import_biz_type_define_map'] = {
    u'权限组': 'import_res_groups'  # 导入权限组/菜单配置项
    }


class JdDataImport(models.Model):
    _name = 'jd.data.import'
    _description = u'数据导入'

    name = fields.Char(string=u'名称', defult=u'初始数据导入')
    manual_operation = fields.Binary(string=u'模板使用手册', attachment=True)
    manual_check = fields.Binary(string=u'数据核对手册', attachment=True)
    template = fields.Binary(string=u'导入模板', attachment=True)
    manual_operation_name = fields.Char(string=u'模板使用手册文件名')
    manual_check_name = fields.Char(string=u'数据核对手册文件名')
    template_name = fields.Char(string=u'导入模板文件名')
    line_ids = fields.One2many('jd.data.import.line', 'import_id', string=u'导入明细')

    @api.multi
    def get_hide_edit_button(self):
        """
        只允许超级管理员账号可编辑该内容
        :return:
        """
        for item in self:
            if self.env.user.id != SUPERUSER_ID:
                item.hide_edit_button = True


class JdDataImportLine(models.Model):
    _name = 'jd.data.import.line'
    _description = u'数据导入明细'
    _order = 'sequence asc'

    import_id = fields.Many2one('jd.data.import', string=u'数据导入', ondelete='cascade')
    sequence = fields.Integer(string=u'导入顺序', required=True)
    biz_type = fields.Char(string=u'单据类型')
    result = fields.Text(string=u'汇总导入结果', compute='_get_import_history')
    time_import = fields.Datetime(string=u'最后导入时间', compute='_get_import_history')
    uid_import = fields.Many2one('res.users', string=u'最后导入人', compute='_get_import_history')
    active = fields.Boolean(string=u'有效', default=True)
    import_history_ids = fields.One2many('jd.data.import.line.history', 'line_id', string=u'导入历史')

    @api.multi
    def new_data_line(self):
        """
        新增导入
        :return:
        """
        item = self[0]
        if item.biz_type not in utils.G_PARAMS['data_import_biz_type_define_map'].keys():
            raise ValidationError(u'该单据类型的导入逻辑尚未定义，请联系您的管理员。')
        if not hasattr(self, utils.G_PARAMS['data_import_biz_type_define_map'][item.biz_type]):
            raise ValidationError(u'该单据类型的导入逻辑尚未定义，请联系您的管理员。')
        return {
            'name': u'新增数据导入',
            'res_model': 'jd.base.wizard.new.data.import.line',
            'view_type': 'form',
            'view_mode': 'form',
            'type': 'ir.actions.act_window',
            'model': 'ir.actions.act_window',
            'search_view_id': False,
            'view_id': False,
            'target': 'new',
            'context': {'import_line_id': item.id,
                        'biz_type': item.biz_type,
                        'sequence': item.sequence}
        }

    @api.multi
    @api.depends('import_history_ids')
    def _get_import_history(self):
        """
        根据对应的导入历史，计算汇总导入结果，最后导入时间与最后导入人
        :return:
        """
        for item in self:

            # 获取该单据类型的导入历史，汇总导入结果代码，获取最后导入时间与导入人
            # 如果没有则都置为空
            if len(item.import_history_ids) == 0:
                item.result, item.time_import, item.uid_import = None, None, None
            else:
                # 最后导入时间与导入人
                last_history = item.import_history_ids[0]
                item.time_import, item.uid_import = last_history.time_import, last_history.uid_import
                # 汇总导入结果：文件数、成功数、失败数
                file_count, success_count, fail_count = 0, 0, 0
                for i in item.import_history_ids:
                    result_dic = ast.literal_eval(i.result_code)
                    if result_dic:
                        file_count += result_dic.get('file_count', 0)
                        success_count += result_dic.get('success_count', 0)
                        fail_count += result_dic.get('fail_count', 0)
                item.result = '文件数：' + str(file_count) + '；' + '成功：' + str(success_count) + '；' + '失败：' + str(fail_count) + '；'

    @api.multi
    def load_file(self):
        """
        验证文件，校验excel文件格式
        TODO:校验Excel中每个cell数据的合理性（并与字段对应，自动匹配）
        :return:
        """
        self.ensure_one()
        if not self.import_file:
            raise ValidationError('请选择文件')
        rows = self._read_file()
        data = [
            list(row) for row in rows
            if any(row)
        ]
        return data

    @api.multi
    def do_import(self):
        """
        执行导入，写入数据:
        【注意：该段逻辑需在业务层定义执行，需在master下覆盖实现，调用各个对应的导入逻辑并返回导入结果，使用BIZ_DEFINE_MAP匹配】
        记录成功失败数目，与每一个失败行的失败原因，反写导入结果，生成导入历史
        :return:
        """
        self.ensure_one()
        result = {
            'success_count': 0,
            'fail_count': 0,
            'fail_reason': {
                'index': 'reason'
            }
        }

        # 判断是否已定义对应方法，调用方法
        if self.biz_type not in utils.G_PARAMS['data_import_biz_type_define_map'].keys():
            raise ValidationError(u'该单据类型的导入逻辑尚未定义')
        if not hasattr(self, utils.G_PARAMS['data_import_biz_type_define_map'][self.biz_type]):
            raise ValidationError(u'该单据类型的导入逻辑尚未定义')
        try:
            # load_file 获取data，data格式为：[list1, list2, list3, ……]
            data = self.load_file()
            result = getattr(self, utils.G_PARAMS['data_import_biz_type_define_map'][self.biz_type])(data)
        except Exception as e:
            msg = u'导入方法执行错误，请联系管理员检查导入逻辑'
            if hasattr(e, 'message') and e.message:
                msg = e.message
            elif hasattr(e, 'name') and e.name:
                msg = e.name
            raise ValidationError(msg)

        # 生成导入历史
        history_val = {
            'line_id': self.id,
            'import_file': self.import_file,
            'file_name': self.file_name,
            'result_code': str(result),
            'time_import': fields.Datetime.now(),
            'uid_import': self.env.user.id
        }
        self.env['jd.data.import.line.history'].create(history_val)

    @api.multi
    def _read_file(self):
        """
        读取文件
        通过其mimetype或者文件类型，调用相应的具体读取文件方法，是一个读取文件的统一入口
        :return:
        """
        self.ensure_one()

        # 从文件内容guess到mimetype
        mimetype = guess_mimetype(self.import_file)
        (file_extension, handler, req) = FILE_TYPE_DICT.get(mimetype, (None, None, None))
        if handler:
            try:
                return getattr(self, '_read_' + file_extension)()
            except Exception:
                _logger.warn("读取文件失败，不能识别的通过文件内容guess到的mimetype")

        # 从文件名获取扩展类型
        if self.file_name:
            p, ext = os.path.splitext(self.file_name)
            if ext in EXTENSIONS:
                try:
                    return getattr(self, '_read_' + ext[1:])()
                except Exception:
                    _logger.warn("读取文件失败，不能识别通过文件名获取到的文件扩展类型")

        if req:
            raise ImportError(_("不能加载 \"{extension}\" 文件: 需要依赖 \"{modname}\"，请联系管理员").format(
                extension=file_extension, modname=req))

        raise ValueError(_("读取文件失败"))

    @api.multi
    def _read_xls(self):
        """
        调用xlrd库读取Excel文件内容
        :return:
        """
        file_binary = base64.b64decode(self.import_file)
        book = xlrd.open_workbook(file_contents=file_binary)
        return self._read_xls_book(book)

    # XLS扩展文件与XLSX文件都执行define：_read_xls()
    _read_xlsx = _read_xls

    def _read_xls_book(self, book):
        """
        获取sheet对象，循环读取每行每个cell的值
        :param book:
        :return:
        """
        sheet = book.sheet_by_index(0)
        for row in itertools.imap(sheet.row, range(sheet.nrows)):
            values = []
            for cell in row:
                if cell.ctype is xlrd.XL_CELL_NUMBER:
                    is_float = cell.value % 1 != 0.0
                    values.append(
                        unicode(cell.value)
                        if is_float
                        else unicode(int(cell.value))
                    )
                elif cell.ctype is xlrd.XL_CELL_DATE:
                    is_datetime = cell.value % 1 != 0.0
                    dt = datetime.datetime(*xlrd.xldate.xldate_as_tuple(cell.value, book.datemode))
                    values.append(
                        dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
                        if is_datetime
                        else dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
                    )
                elif cell.ctype is xlrd.XL_CELL_BOOLEAN:
                    values.append(u'True' if cell.value else u'False')
                elif cell.ctype is xlrd.XL_CELL_ERROR:
                    raise ValueError(
                        _("读取Excel时发现错误的单元格: %s") %
                        xlrd.error_text_from_code.get(
                            cell.value, "不能识别的错误代码 %s" % cell.value)
                    )
                else:
                    values.append(cell.value)
            if any(x for x in values if x.strip()):
                yield values

    ########################################################################################################
    # 以下是导数逻辑
    ########################################################################################################

    def import_res_groups(self, data):
        """
        第一行：从第三列开始为群组名称，用于创建群组
        第二行开始：
        [0]、完整路径（用来匹配菜单项，需要检查对应模型的reference字段）
        [1]、菜单
        [列数]、权限项
        :param data:
        :return:
        """
        # 取第一行，从第三列开始，创建群组，获取group_dic
        group_list = data[0]
        list_len = len(group_list)
        group_dic = {}
        for i in range(2, list_len):
            if not group_list[i]:
                raise ValidationError(u'群组名称不能为空，第%s列' % str(i + 1))
            # 先判断该name的群组是否存在
            group_id = self.env['res.groups'].sudo().search([('name', '=', group_list[i].strip())]).id
            if not group_id:
                group_id = self.env['res.groups'].sudo().create({'name': group_list[i].strip()}).id
            group_dic.update({
                i: group_id
            })

        ###########################################################################
        # 数据导入逻辑
        ###########################################################################

        # 第二行数据开始，从第三列解析单元格，拼凑名称查找菜单项配置
        success_count, fail_count = 0, 0
        fail_reason = {}
        cr = self.env.cr
        for line in range(1, len(data)):
            list = data[line]
            for row in range(2, list_len):
                try:
                    cell = list[row].strip().split('/') if list[row] else None
                    menu_name = list[0].strip() if list[0] else None

                    # 清除该群组下该菜单的关联菜单项配置，执行删除sql
                    empty_sql = """
                    delete from jd_menu_config_base_res_group
                    where left_id = %s
                    and right_id in (
                    select
                    id
                    from jd_ir_ui_menu_config
                    where name like %s
                    )
                    """
                    cr.execute(empty_sql, (group_dic[row], str(menu_name) + '-%'))

                    # 有权限项时，遍历权限项，拼凑menu_names，执行写入
                    if cell:
                        menu_names = [(menu_name + '-' + str(item)) for item in cell]
                        menu_sql = """
                        select
                        id 
                        from
                        jd_ir_ui_menu_config
                        where
                        name in %s
                        """
                        cr.execute(menu_sql, (tuple(menu_names),))
                        menu_result = cr.dictfetchall()
                        menu_ids = [item['id'] for item in menu_result]

                        if len(menu_ids) > 0:
                            value_sql = """
                            insert into jd_menu_config_base_res_group
                            (left_id, right_id)
                            values
                            """
                            for menu_id in menu_ids:
                                value_sql += "(%s, %s)," % (group_dic[row], menu_id)
                            value_sql = value_sql[:-1] + ';'
                            cr.execute(value_sql)
                    success_count += 1
                except Exception as e:
                    msg = e.message if e.message else e.name
                    fail_count += 1
                    fail_reason.update({str(line + 1) + str(row + 1): msg})

        ###########################################################################
        # 根据导入结果，执行初始化ir.model.access
        ###########################################################################

        for i in group_dic:

            try:
                acl_list = self.env['res.groups'].browse(group_dic[i]).create_model_acl_by_menu_ids()
                if acl_list:
                    self.env['wizard.jd.access.controls.config'].remove_duplicate(acl_list)
            except Exception as e:
                msg = e.message if e.message else e.name
                fail_count += 1
                fail_reason.update({str(i + 1) + '0000': msg})

        self.env['ir.model.access'].call_cache_clearing_methods()

        return {
            'success_count': success_count,
            'fail_count': fail_count,
            'fail_reason': fail_reason
        }


class JdDataImportLineHistory(models.Model):
    _name = 'jd.data.import.line.history'
    _description = u'数据导入历史'
    _order = 'id desc'

    line_id = fields.Many2one('jd.data.import.line', string=u'对应导入明细', ondelete='cascade')
    biz_type = fields.Char(string=u'单据类型', related='line_id.biz_type')
    result = fields.Text(string=u'导入结果', compute='_get_result')
    result_code = fields.Char(string=u'导入结果代码', required=True)
    # result_code = {
    #     'file_count': 0,
    #     'success_count': 0,
    #     'fail_count': 0,
    #     'pass_count': 0
    # }
    # 根据result_code计算出要显示给用户看的导入结果，此处只显示数量不显示明细
    time_import = fields.Datetime(string=u'导入时间')
    uid_import = fields.Many2one('res.users', string=u'导入人')
    line_ids = fields.One2many('jd.data.import.line.history.line', 'parent_id', string=u'导入历史明细')

    @api.multi
    @api.depends('result_code')
    def _get_result(self):
        """
        根据导入结果代码，获取要展示给用户看的导入结果
        [文件数：xx；成功：xx；失败：xx；]
        :return:
        """
        for item in self:
            result = ''
            result_dic = ast.literal_eval(item.result_code)
            if result_dic:
                result = '文件数：' + str(result_dic.get('file_count', ' ')) + '；' + '成功：' +\
                         str(result_dic.get('success_count', ' ')) + '；' + '失败：' +\
                         str(result_dic.get('fail_count', ' ')) + '；\r\n'
            item.result = result


class JdDataImportLineHistoryLine(models.Model):
    _name = 'jd.data.import.line.history.line'
    _description = u'数据导入历史明细'
    _order = 'id asc'

    parent_id = fields.Many2one('jd.data.import.line.history', string=u'数据导入历史', ondelete='cascade')
    import_file = fields.Binary(string=u'导入文件', attachment=True)
    file_name = fields.Char(string=u'文件名称')
    result = fields.Text(string=u'导入结果', compute='_get_result')
    result_tree = fields.Text(string=u'导入结果', compute='_get_result')
    result_code = fields.Char(string=u'导入结果代码')
    error_msg = fields.Char(string=u'错误代码')
    # result_code = {
    #     'success_count': 0,
    #     'fail_count': 0,
    #     'pass_count': 0,
    #     'fail_reason': {
    #         'index': 'reason'
    #     },
    #     'pass_index': '1、2、3'
    # }
    # 根据result_code计算出要显示给用户看的导入结果

    @api.depends('result_code')
    @api.multi
    def _get_result(self):
        """
        根据导入结果代码获取导入结果
        :return:
        """
        for item in self:
            result = ''
            item.result_tree = None
            if item.error_msg:
                result = item.error_msg
            else:
                if item.result_code:
                    result_dic = ast.literal_eval(item.result_code)
                    if result_dic:
                        result = '成功：' + str(result_dic['success_count']) + '；' + '失败：' + str(result_dic['fail_count']) + '；'
                        if result_dic.get('pass_count'):
                            result += ('跳过：' + result_dic['pass_count'] + '；')
                        item.result_tree = result
                        result += '\r\n'
                        if result_dic.get('fail_reason'):
                            fail_reason = ''
                            dict_fail_reason = result_dic['fail_reason']
                            for key in sorted(dict_fail_reason.keys()):
                                fail_reason += '(第%s行：%s)\r\n' % (str((key)), dict_fail_reason.get(key))
                            result += '导入失败原因：\r\n' + fail_reason
                        if result_dic.get('pass_index'):
                            result += ('\r\n' + '跳过行号：' + result_dic['pass_index'])
            item.result = result
