import csv
import logging
import ntpath
import re
import sys
from uuid import UUID

from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.core.exceptions import ValidationError
from django.core.management.base import CommandError
from django.utils import translation
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy

from kolibri.core.auth.constants import role_kinds
from kolibri.core.auth.constants.collection_kinds import CLASSROOM
from kolibri.core.auth.constants.demographics import choices
from kolibri.core.auth.constants.demographics import DEFERRED
from kolibri.core.auth.models import Classroom
from kolibri.core.auth.models import Facility
from kolibri.core.auth.models import FacilityUser
from kolibri.core.auth.models import Membership
from kolibri.core.tasks.management.commands.base import AsyncCommand
from kolibri.core.tasks.utils import get_current_job
from kolibri.core.utils.csv import open_csv_for_reading

try:
    FileNotFoundError
except NameError:
    FileNotFoundError = IOError

logger = logging.getLogger(__name__)

# TODO: decide whether these should be internationalized
fieldnames = (
    "UUID",
    "USERNAME",
    "PASSWORD",
    "FULL_NAME",
    "USER_TYPE",
    "IDENTIFIER",
    "BIRTH_YEAR",
    "GENDER",
    "ENROLLED_IN",
    "ASSIGNED_TO",
)


# These constants must be entered vertbatim in the CSV
roles_map = {
    "LEARNER": None,
    "ADMIN": role_kinds.ADMIN,
    "FACILITY_COACH": role_kinds.COACH,
    "CLASS_COACH": role_kinds.ASSIGNABLE_COACH,
}

# Error messages ###
UNEXPECTED_EXCEPTION = 0
TOO_LONG = 1
INVALID = 2
DUPLICATED_USERNAME = 3
INVALID_USERNAME = 4
REQUIRED_COLUMN = 5
INVALID_HEADER = 6
NO_FACILITY = 7
FILE_READ_ERROR = 8
FILE_WRITE_ERROR = 9
REQUIRED_PASSWORD = 10
NON_EXISTENT_UUID = 11
INVALID_UUID = 12

MESSAGES = {
    UNEXPECTED_EXCEPTION: pgettext_lazy(
        "Error message that might appear when there's a programming error importing a CSV file",
        "Unexpected error [{}]: {}",
    ),
    TOO_LONG: pgettext_lazy(
        "Error when the command is executed in the Terminal (command prompt)",
        "Value in column '{}' has too many characters",
    ),
    INVALID: _("Invalid value in column '{}'"),
    DUPLICATED_USERNAME: _("Username is duplicated"),
    INVALID_USERNAME: _(
        "Username only can contain characters, numbers and underscores"
    ),
    REQUIRED_COLUMN: pgettext_lazy(
        "Error message indicating that the CSV file selected for import is missing a required column",
        "The column '{}' is required",
    ),
    INVALID_HEADER: pgettext_lazy(
        "Error message indicating that one column header in the CSV file selected for import is missing or incorrect",
        "Invalid header label found in the first row",
    ),
    NO_FACILITY: pgettext_lazy(
        "Error when the command is executed in the Terminal (command prompt)",
        "No default facility exists. Make sure to set up a facility on the  device before importing users and classes",
    ),
    FILE_READ_ERROR: _("Error trying to read csv file: {}"),
    FILE_WRITE_ERROR: _("Error trying to write csv file: {}"),
    REQUIRED_PASSWORD: _(
        "The password field is required. To leave the password unchanged in existing users, insert an asterisk (*)"
    ),
    NON_EXISTENT_UUID: _(
        "Cannot update user with ID: '{}' because no user with that database ID exists in this facility"
    ),
    INVALID_UUID: _("Database ID is not valid"),
}

# Validators ###


def number_range(min, max, allow_null=False):
    """
    Return a value check function which raises a ValueError if the supplied
    value is less than `min` or greater than `max`.
    """

    def checker(v):
        if v == DEFERRED:
            return checker
        if allow_null and not v:
            return None

        if v is None or (int(v) < min or int(v) > max):
            raise ValueError(v)

    return checker


def not_empty():
    """
    Return a value check function which raises a ValueError if the supplied
    value is None or an empty string
    """

    def checker(v):
        if v is None:
            raise ValueError(v)
        if len(v) == 0:
            raise ValueError(v)

    return checker


def value_length(length, allow_null=False, multiple=False):
    """
    Return a value check function which raises a ValueError if the supplied
    value has a length greater than 'length'
    If null is not True raises a ValueError if the supplied value is None.
    If multiple is True it checks the length of each of the separated by commas value
    """

    def checker(v):
        def check_single_value(v):
            if v is None or len(v) > length:
                raise ValueError(v)

        if v == DEFERRED:
            return checker
        if allow_null and v is None:
            return None
        if multiple:
            values = v.split(",")
            for value in values:
                check_single_value(value)
        else:
            check_single_value(v)

    return checker


def valid_uuid(allow_null=True):
    """
    Return a value check function which raises a ValueError if the supplied
    value is ot a valid uuid
    """

    def checker(v):
        if allow_null and (v is None or v == ""):
            return None
        try:
            UUID(v).version
        except (ValueError, TypeError):
            raise ValueError(v)

    return checker


def enumeration(*args):
    """
    Return a value check function which raises a ValueError if the value (case
    insensitive) is not in the enumeration of values provided by args.
    """
    if len(args) == 1:
        # assume the first argument defines the membership
        members = args[0].lower()
    else:
        members = tuple(map(str.lower, args))

    def checker(value):
        if value == DEFERRED:
            return checker
        if value.lower() not in members:
            raise ValueError(value)

    return checker


def valid_name(username=True, allow_null=False):
    """
    Return a value check function which raises a ValueError if the value has
    some of the punctuaction chars that are not allowed.
    If username is False it allows spaces, slashes and hyphens.
    If null is not True raises a ValueError if the supplied value is None.
    """

    def checker(v):
        if allow_null and v is None:
            return checker
        if v is None:
            raise ValueError(v)
        has_punc = "[\s`~!@#$%^&*()\-+={}\[\]\|\\\/:;\"'<>,\.\?]"  # noqa
        if not username:
            has_punc = "[`~!@#$%^&*()\+={}\[\]\|\\\/:;\"'<>\.\?]"  # noqa
        if re.search(has_punc, v):
            raise ValueError(v)

    return checker


def reverse_dict(original):
    """
    Returns a dictionary based on an original dictionary
    where previous keys are values and previous values pass
    to be the keys
    """
    final = {}
    for k, value in original.items():
        if type(value) == list:
            for v in value:
                final.setdefault(v, []).append(k)
        else:
            final.setdefault(value, []).append(k)
    return final


class Validator(object):
    """
    Class to apply different validation checks on a CSV data reader.
    """

    def __init__(self, header_translation):
        self._checks = list()
        self.classrooms = dict()
        self.coach_classrooms = dict()
        self.users = dict()
        self.header_translation = header_translation
        self.roles = {r: list() for r in roles_map.values() if r is not None}

    def add_check(self, header_name, check, messag
e):
        """
        Add a header check, i.e., check whether the header record is consistent
        with the expected field names.
        `header_name` - name of the header for the column to be c
hecked
        `check`- function to be used as validator of the values in the column
        `message` - problem message to report if a value is not valid
        """
        self._checks.append((header_name, check, message))

    def get_username(self, row):
        username = row.get(self.header_translation