# -*- coding: utf-8 -*-
# (c) Nano Nano Ltd 2019

import sys
import csv
from decimal import Decimal, InvalidOperation

from colorama import Fore, Back
from tqdm import tqdm, trange
import dateutil.parser
import xlrd

from .config import config
from .transactions import Buy, Sell
from .record import TransactionRecord
from .exceptions import TransactionParserError, UnexpectedTransactionTypeError, \
                        TimestampParserError, DataValueError, MissingDataError, \
                        UnexpectedDataError

class ImportRecords(object):
    def __init__(self):
        self.t_rows = []
        self.success_cnt = 0
        self.failure_cnt = 0

    def import_excel(self, filename):
        workbook = xlrd.open_workbook(filename)
        print("%sExcel file: %s%s" % (Fore.WHITE, Fore.YELLOW, filename))

        for worksheet in workbook.sheets():
            if config.args.debug:
                print("%simporting '%s' rows" % (Fore.CYAN, worksheet.name))

            for row_num in trange(0, worksheet.nrows,
                                  unit=' row',
                                  desc="%simporting '%s' rows%s" % (
                                      Fore.CYAN, worksheet.name, Fore.GREEN),
                                  disable=bool(config.args.debug or not sys.stdout.isatty())):
                if row_num == 0:
                    # skip headers
                    continue

                row = [self.convert_cell(worksheet.cell(row_num, cell_num), workbook)
                       for cell_num in range(0, worksheet.ncols)]

                t_row = TransactionRow(row[:len(TransactionRow.HEADER)], row_num+1, worksheet.name)
                try:
                    t_row.parse()
                except TransactionParserError as e:
                    t_row.failure = e

                if config.args.debug or t_row.failure:
                    tqdm.write("%simport: %s" % (Fore.YELLOW, t_row))

                if t_row.failure:
                    tqdm.write("%sERROR%s %s" % (
                        Back.RED+Fore.BLACK, Back.RESET+Fore.RED, t_row.failure))

                self.t_rows.append(t_row)
                self.update_cnts(t_row)

        workbook.release_resources()
        del workbook

    @staticmethod
    def convert_cell(cell, workbook):
        if cell.ctype == xlrd.XL_CELL_DATE:
            value = xlrd.xldate.xldate_as_datetime(cell.value, workbook.datemode). \
                         strftime('%Y-%m-%d %H:%M:%S')
        elif cell.ctype in (xlrd.XL_CELL_NUMBER, xlrd.XL_CELL_BOOLEAN, xlrd.XL_CELL_ERROR):
            # repr is required to ensure no precision is lost
            value = repr(cell.value)
        else:
            if sys.version_info[0] >= 3:
                value = str(cell.value)
            else:
                value = cell.value.encode('utf-8')

        return value

    def import_csv(self, import_file):
        print("%sCSV file: %s%s" % (Fore.WHITE, Fore.YELLOW, import_file.name))
        if config.args.debug:
            print("%simporting rows" % Fore.CYAN)

        if sys.version_info[0] < 3:
            # Special handling required for utf-8 encoded csv files
            reader = csv.reader(self.utf_8_encoder(import_file))
        else:
            reader = csv.reader(import_file)

        for row in tqdm(reader,
                        unit=' row',
                        desc="%simporting%s" % (Fore.CYAN, Fore.GREEN),
                        disable=bool(config.args.debug or not sys.stdout.isatty())):
            if reader.line_num == 1:
                # skip headers
                continue

            t_row = TransactionRow(row[:len(TransactionRow.HEADER)], reader.line_num)
            try:
                t_row.parse()
            except TransactionParserError as e:
                t_row.failure = e

            if config.args.debug or t_row.failure:
                tqdm.write("%simport: %s" % (Fore.YELLOW, t_row))

            if t_row.failure:
                tqdm.write("%sERROR%s %s" % (
                    Back.RED+Fore.BLACK, Back.RESET+Fore.RED, t_row.failure))

            self.t_rows.append(t_row)
            self.update_cnts(t_row)

    @staticmethod
    def utf_8_encoder(unicode_csv_data):
        for line in unicode_csv_data:
            yield line.encode('utf-8')

    def update_cnts(self, t_row):
        if t_row.failure is not None:
            self.failure_cnt += 1
        elif t_row.t_record is not None:
            self.success_cnt += 1

    def get_records(self):
        transaction_records = [t_row.t_record for t_row in self.t_rows if t_row.t_record]

        transaction_records.sort()
        for t_record in transaction_records:
            t_record.set_tid()

        if config.args.debug:
            for t_row in self.t_rows:
                print("%simport: %s" % (Fore.YELLOW, t_row))

        return transaction_records

class TransactionRow(object):
    HEADER = ['Type',
              'Buy Quantity', 'Buy Asset', 'Buy Value',
              'Sell Quantity', 'Sell Asset', 'Sell Value',
              'Fee Quantity', 'Fee Asset', 'Fee Value',
              'Wallet', 'Timestamp']

    BUY_TYPES = (TransactionRecord.TYPE_DEPOSIT,
                 TransactionRecord.TYPE_MINING,
                 TransactionRecord.TYPE_STAKING,
                 TransactionRecord.TYPE_INTEREST,
                 TransactionRecord.TYPE_DIVIDEND,
                 TransactionRecord.TYPE_INCOME,
                 TransactionRecord.TYPE_GIFT_RECEIVED)
    SELL_TYPES = (TransactionRecord.TYPE_WITHDRAWAL,
                  TransactionRecord.TYPE_SPEND,
                  TransactionRecord.TYPE_GIFT_SENT,
                  TransactionRecord.TYPE_GIFT_SPOUSE,
                  TransactionRecord.TYPE_CHARITY_SENT)

    TRANSFER_TYPES = (TransactionRecord.TYPE_DEPOSIT,
                      TransactionRecord.TYPE_WITHDRAWAL)

    cnt = 0

    def __init__(self, row, row_num, worksheet_name=None):
        self.row = row
        self.row_num = row_num
        self.worksheet_name = worksheet_name
        self.t_record = None
        self.failure = None

    def parse(self):
        if all(not self.row[i] for i in range(len(self.row))):
            # Skip empty rows
            return

        buy = sell = fee = None
        buy_asset = sell_asset = fee_asset = None

        t_type = self.row[0]
        if t_type in self.BUY_TYPES:
            buy_quantity, buy_asset, buy_value = self.validate_buy(self.row)
            self.validate_no_sell(self.row)
            fee_quantity, fee_asset, fee_value = self.validate_fee(self.row)
        elif t_type in self.SELL_TYPES:
            self.validate_no_buy(self.row)
            sell_quantity, sell_asset, sell_value = self.validate_sell(self.row)
            fee_quantity, fee_asset, fee_value = self.validate_fee(self.row)
        elif t_type == TransactionRecord.TYPE_TRADE:
            buy_quantity, buy_asset, buy_value = self.validate_buy(self.row)
            sell_quantity, sell_asset, sell_value = self.validate_sell(self.row)
            fee_quantity, fee_asset, fee_value = self.validate_fee(self.row)
        else:
            raise UnexpectedTransactionTypeError(0, self.HEADER[0], t_type)

        if buy_asset:
            buy = Buy(t_type, buy_quantity, buy_asset, buy_value)
        if sell_asset:
            sell = Sell(t_type, sell_quantity, sell_asset, sell_value)
        if fee_asset:
            # Fees are added as a separate spend transaction
            fee = Sell(TransactionRecord.TYPE_SPEND, fee_quantity, fee_asset, fee_value)
            if t_type in self.TRANSFER_TYPES:
                # A fee spend is normally a disposal unless it's part of a transfer
                fee.disposal = False

        self.t_record = TransactionRecord(t_type, buy, sell, fee, self.row[10],
                                          self.parse_timestamp(self.row[11]))

    @staticmethod
    def parse_timestamp(timestamp_str):
        try:
            timestamp = dateutil.parser.parse(timestamp_str, tzinfos=config.TZ_INFOS)
        except ValueError:
            raise TimestampParserError(11, TransactionRow.HEADER[11], timestamp_str)

        if timestamp.tzinfo is None:
            # Default to UTC if no timezone is specified
            timestamp = timestamp.replace(tzinfo=config.TZ_UTC)

        return timestamp

    @staticmethod
    def validate_buy(row):
        if row[1]:
            try:
                buy_quantity = Decimal(TransactionRow.strip_non_digits(row[1]))
            except InvalidOperation:
                raise DataValueError(1, TransactionRow.HEADER[1], row[1])

            if buy_quantity < 0:
                raise DataValueError(1, TransactionRow.HEADER[1], buy_quantity)
        else:
            raise MissingDataError(1, TransactionRow.HEADER[1])

        if row[2]:
            buy_asset = row[2]
        else:
            raise MissingDataError(2, TransactionRow.HEADER[2])

        if row[3]:
            try:
                buy_value = Decimal(TransactionRow.strip_non_digits(row[3]))
            except InvalidOperation:
                raise DataValueError(3, TransactionRow.HEADER[3], row[3])

            if buy_value < 0:
                raise DataValueError(3, TransactionRow.HEADER[3], buy_value)

            if buy_asset == config.CCY and buy_value != buy_quantity:
                raise DataValueError(3, TransactionRow.HEADER[3], buy_value)
        else:
            buy_value = None

        return buy_quantity, buy_asset, buy_value

    @staticmethod
    def validate_no_buy(row):
        if row[1]:
            raise UnexpectedDataError(1, TransactionRow.HEADER[1], row[1])

        if row[2]:
            raise UnexpectedDataError(2, TransactionRow.HEADER[2], row[2])

        if row[3]:
            raise UnexpectedDataError(3, TransactionRow.HEADER[3], row[3])

    @staticmethod
    def validate_sell(row):
        if row[4]:
            try:
                sell_quantity = Decimal(TransactionRow.strip_non_digits(row[4]))
            except InvalidOperation:
                raise DataValueError(4, TransactionRow.HEADER[4], row[4])

            if sell_quantity < 0:
                raise DataValueError(4, TransactionRow.HEADER[4], sell_quantity)
        else:
            raise MissingDataError(4, TransactionRow.HEADER[4])

        if row[5]:
            sell_asset = row[5]
        else:
            raise MissingDataError(5, TransactionRow.HEADER[5])

        if row[6]:
            try:
                sell_value = Decimal(TransactionRow.strip_non_digits(row[6]))
            except InvalidOperation:
                raise DataValueError(6, TransactionRow.HEADER[6], row[6])

            if sell_value < 0:
                raise DataValueError(6, TransactionRow.HEADER[6], sell_value)

            if sell_asset == config.CCY and sell_value != sell_quantity:
                raise DataValueError(6, TransactionRow.HEADER[6], sell_value)
        else:
            sell_value = None

        return sell_quantity, sell_asset, sell_value

    @staticmethod
    def validate_no_sell(row):
        if row[4]:
            raise UnexpectedDataError(4, TransactionRow.HEADER[4], row[4])

        if row[5]:
            raise UnexpectedDataError(5, TransactionRow.HEADER[5], row[5])

        if row[6]:
            raise UnexpectedDataError(6, TransactionRow.HEADER[6], row[6])

    @staticmethod
    def validate_fee(row):
        if row[7]:
            try:
                fee_quantity = Decimal(TransactionRow.strip_non_digits(row[7]))
            except InvalidOperation:
                raise DataValueError(7, TransactionRow.HEADER[7], row[7])

            if fee_quantity < 0:
                raise DataValueError(7, TransactionRow.HEADER[7], fee_quantity)
        else:
            fee_quantity = None

        if row[8]:
            fee_asset = row[8]
        else:
            fee_asset = None

        if row[9]:
            try:
                fee_value = Decimal(TransactionRow.strip_non_digits(row[9]))
            except InvalidOperation:
                raise DataValueError(9, TransactionRow.HEADER[9], row[9])

            if fee_value < 0:
                raise DataValueError(9, TransactionRow.HEADER[9], fee_value)

            if fee_asset == config.CCY and fee_value != fee_quantity:
                raise DataValueError(9, TransactionRow.HEADER[9], fee_value)
        else:
            fee_value = None

        if fee_quantity is not None and not fee_asset:
            raise MissingDataError(8, TransactionRow.HEADER[8])
        elif fee_quantity is None and fee_asset:
            raise MissingDataError(7, TransactionRow.HEADER[7])

        return fee_quantity, fee_asset, fee_value

    @staticmethod
    def strip_non_digits(string):
        return string.strip('£€$').replace(',', '')

    def __str__(self):
        if self.t_record and self.t_record.tid:
            tid_str = " %s[TID:%s]" % (Fore.MAGENTA, self.t_record.tid[0])
        else:
            tid_str = ''

        if self.worksheet_name:
            worksheet_str = "'%s' " % self.worksheet_name
        else:
            worksheet_str = ''

        if self.failure is not None:
            row_str = ', '.join(["%s'%s'%s" % (Back.RED, data, Back.RESET)
                                 if self.failure.col_num == num
                                 else "'%s'" % data
                                 for num, data in enumerate(self.row)])
        else:
            row_str = "'%s'" % '\', \''.join(self.row)

        return "%srow[%s] [%s]%s" % (
            worksheet_str,
            self.row_num,
            row_str,
            tid_str)
