# -*- coding: UTF-8 -*-

# Copyright (C) 2015-2016 Avencall
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


import re
import time
from collections import namedtuple
from typing import Any

from flask import request
from xivo.unicode_csv import UnicodeDictReader
from xivo_dao.helpers import errors

ParseRule = namedtuple('ParseRule', ['csv_name', 'parser', 'name'])
ALPHANUMERIC = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*?[0-9])(?=.*[\W_])[A-Za-z0-9\W_][^\"\'\\\<\>\s]{1,}$"


class CsvParser(object):

    def __init__(self, lines, encoding):
        self.reader = UnicodeDictReader(lines)

    def __iter__(self):
        return CsvIterator(self.reader)


class CsvIterator(object):

    def __init__(self, reader: UnicodeDictReader):
        self.reader = reader
        self.position = 0
        self.warnings = []

    def __next__(self):
        row = next(self.reader)
        self.position += 1
        self.warnings = []
        return CsvRow(row, self.position, self.warnings)


class Rule(object):

    def __init__(self, csv_name, name):
        self.csv_name = csv_name
        self.name = name
        self.warnings = []

    def insert(self, fields, entry):
        if self.csv_name in fields:
            value = fields.get(self.csv_name, "")
            entry[self.name] = self.parse(value)

    def get_warnings(self):
        current_warnings = self.warnings
        self.warnings = []
        return current_warnings


class UnicodeRule(Rule):

    def parse(self, value):
        if value == "":
            return None
        return value


class LowercaseUnicodeRule(Rule):

    def parse(self, value):
        if value == "":
            return None
        return value.lower()


class BooleanRule(Rule):

    def parse(self, value):
        if value == "":
            return None
        if value not in ("0", "1"):
            raise errors.invalid_choice(self.csv_name, ["0", "1"])
        return value == "1"


class IntRule(Rule):

    def parse(self, value):
        if value == "":
            return None
        if not value.isdigit():
            raise errors.wrong_type(self.csv_name, 'integer')
        return int(value)


class ColonListRule(Rule):

    def parse(self, value):
        if value == "":
            return []
        return [entry.strip() for entry in value.split(";")]


class DictRule(Rule):
    _BAD_LIST_SEPARATOR = re.compile(r"=[^;]*[:/|,][^;]*=")

    def parse(self, value) -> dict[str,Any]:
        value = (value or "").strip()
        if value == "":
            return {}

        if self._BAD_LIST_SEPARATOR.search(value):
            raise errors.wrong_type(self.csv_name, "key=value;key2=value2")

        entries = {}
        for entry in value.split(";"):
            key, sep, val = entry.partition("=")
            if sep != "=":
                raise errors.wrong_type(self.csv_name, "key=value;key2=value2")

            key = key.strip()
            val = val.strip()
            if not key or not val:
                raise errors.wrong_type(self.csv_name, "key=value;key2=value2")

            entries[key] = val

        return entries


class PasswordPolicyUnicodeRule(Rule):

    def parse(self, value):
        if value == "":
            return None
        if not re.match(ALPHANUMERIC, value):
            self.warnings.append("Password should contain lowercase and uppercase letters, numbers and special characters")
        if not len(value) >= 8:
            self.warnings.append("Password should contain at least 8 characters")
        return value


class CsvRow(object):
    USER_RULES = (
        UnicodeRule('uuid', 'uuid'),
        UnicodeRule('firstname', 'firstname'),
        UnicodeRule('lastname', 'lastname'),
        UnicodeRule('email', 'email'),
        UnicodeRule('username', 'username'),
        PasswordPolicyUnicodeRule('password', 'password'),
        UnicodeRule('language', 'language'),
        UnicodeRule('outgoing_caller_id', 'outgoing_caller_id'),
        UnicodeRule('mobile_phone_number', 'mobile_phone_number'),
        UnicodeRule('call_permission_password', 'call_permission_password'),
        UnicodeRule('userfield', 'userfield'),
        BooleanRule('enabled', 'enabled'),
        BooleanRule('supervision_enabled', 'supervision_enabled'),
        BooleanRule('call_transfer_enabled', 'call_transfer_enabled'),
        BooleanRule('call_record_enabled', 'call_record_enabled'),
        BooleanRule('online_call_record_enabled', 'online_call_record_enabled'),
        IntRule('entity_id', 'entity_id'),
        IntRule('ring_seconds', 'ring_seconds'),
        IntRule('simultaneous_calls', 'simultaneous_calls'),
        ColonListRule('labels', 'labels'),
    )

    VOICEMAIL_RULES = (
        UnicodeRule('voicemail_name', 'name'),
        UnicodeRule('voicemail_number', 'number'),
        UnicodeRule('voicemail_context', 'context'),
        UnicodeRule('voicemail_password', 'password'),
        UnicodeRule('voicemail_email', 'email'),
        BooleanRule('voicemail_attach_audio', 'attach_audio'),
        BooleanRule('voicemail_delete_messages', 'delete_messages'),
        BooleanRule('voicemail_ask_password', 'ask_password'),
    )

    LINE_RULES = (
        UnicodeRule('line_protocol', 'endpoint'),
        UnicodeRule('context', 'context'),
        LowercaseUnicodeRule('line_site', 'registrar'),
    )

    SIP_RULES = (
        LowercaseUnicodeRule('sip_username', 'name'),
        UnicodeRule('sip_secret', 'secret'),
        DictRule('sip_options', 'sip_options'),
        UnicodeRule('line_protocol', 'endpoint'),
    )

    EXTENSION_RULES = (
        UnicodeRule('exten', 'exten'),
        UnicodeRule('context', 'context'),
    )

    CTI_PROFILE_RULES = (
        UnicodeRule('cti_profile_name', 'name'),
        BooleanRule('cti_profile_enabled', 'enabled'),
    )

    INCALL_RULES = (
        UnicodeRule('incall_exten', 'exten'),
        UnicodeRule('incall_context', 'context'),
        IntRule('incall_ring_seconds', 'ring_seconds'),
    )

    CALL_PERMISSION_RULES = (
        ColonListRule('call_permissions', 'names'),
    )

    IGNORED_RULES = (
        UnicodeRule('provisioning_code', 'provisioning_code'),
    )

    def __init__(self, fields, position: int, warnings: list[Any]):
        self.fields = fields
        self.position = position
        self.warnings = warnings

    def get_warnings(self):
        current_warnings = self.warnings
        self.warnings = []
        return current_warnings

    @staticmethod
    def allowed_header():
        default_csv_header = []
        rules = [CsvRow.VOICEMAIL_RULES, CsvRow.USER_RULES, CsvRow.LINE_RULES, CsvRow.SIP_RULES, CsvRow.EXTENSION_RULES,
                 CsvRow.CTI_PROFILE_RULES, CsvRow.INCALL_RULES, CsvRow.CALL_PERMISSION_RULES, CsvRow.IGNORED_RULES]
        for rule_tuple in rules:
            for rule_object in rule_tuple:
                default_csv_header.append(rule_object.csv_name)
        return default_csv_header

    def parse(self) -> dict[str, dict[str, Any]]:
        return {
            'user': self.parse_rules(self.USER_RULES),
            'voicemail': self.parse_rules(self.VOICEMAIL_RULES),
            'line': self.parse_rules(self.LINE_RULES),
            'sip': self.parse_rules(self.SIP_RULES),
            'extension': self.parse_rules(self.EXTENSION_RULES),
            'incall': self.parse_rules(self.INCALL_RULES),
            'cti_profile': self.parse_rules(self.CTI_PROFILE_RULES),
            'call_permissions': self.parse_rules(self.CALL_PERMISSION_RULES),
            'sccp': {},
        }

    def parse_rules(self, rules):
        entry = {}
        for rule in rules:
            rule.insert(self.fields, entry)
            self.warnings.append(rule.get_warnings())
        return entry

    def format_error(self, exc):
        return {'message': str(exc),
                'timestamp': int(time.time()),
                'details': {'row': self.fields,
                            'row_number': self.position}}


def get_incorrect_header(lines, allowed_header):
    incorrect_header_columns = []
    for header_column in lines[0].split(','):
        if header_column.rstrip() not in allowed_header:
            incorrect_header_columns.append(header_column)
    return incorrect_header_columns


def parse():
    allowed_header = CsvRow.allowed_header()
    charset = request.mimetype_params.get('charset', 'utf-8')
    lines = request.data.decode().split("\n")
    incorrect_header_columns = get_incorrect_header(lines, allowed_header)
    if incorrect_header_columns:
        raise errors.csv_import_unknown_field(incorrect_header_columns, "Unknown column name")
    else:
        return CsvParser(lines, charset)
