from schematics import Model
from schematics.exceptions import ValidationError
from schematics.types import (
    StringType,
    BaseType,
    IntType,
    BooleanType,
    FloatType,
    UTCDateTimeType,
    DateType,
)
from schematics.types.compound import ListType, ModelType
from backend.models.dtos.task_annotation_dto import TaskAnnotationDTO
from backend.models.dtos.user_dto import is_known_mapping_level
from backend.models.dtos.stats_dto import Pagination
from backend.models.dtos.team_dto import ProjectTeamDTO
from backend.models.dtos.interests_dto import InterestDTO
from backend.models.postgis.statuses import (
    ProjectStatus,
    ProjectPriority,
    MappingTypes,
    TaskCreationMode,
    Editors,
    MappingPermission,
    ValidationPermission,
)
from backend.models.dtos.campaign_dto import CampaignDTO


def is_known_project_status(value):
    """ Validates that Project Status is known value """
    if type(value) == list:
        return  # Don't validate the entire list, just the individual values

    try:
        ProjectStatus[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown projectStatus: {value} Valid values are {ProjectStatus.DRAFT.name}, "
            f"{ProjectStatus.PUBLISHED.name}, {ProjectStatus.ARCHIVED.name}"
        )


def is_known_project_priority(value):
    """ Validates Project priority is known value """
    try:
        ProjectPriority[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown projectStatus: {value} Valid values are {ProjectPriority.LOW.name}, "
            f"{ProjectPriority.MEDIUM.name}, {ProjectPriority.HIGH.name}, "
            f"{ProjectPriority.URGENT.name}"
        )


def is_known_mapping_type(value):
    """ Validates Mapping Type is known value"""
    if type(value) == list:
        return  # Don't validate the entire list, just the individual values

    try:
        MappingTypes[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown mappingType: {value} Valid values are {MappingTypes.ROADS.name}, "
            f"{MappingTypes.BUILDINGS.name}, {MappingTypes.WATERWAYS.name}, "
            f"{MappingTypes.LAND_USE.name}, {MappingTypes.OTHER.name}"
        )


def is_known_editor(value):
    """ Validates Editor is known value"""
    if type(value) == list:
        return  # Don't validate the entire list, just the individual values

    try:
        Editors[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown editor: {value} Valid values are {Editors.ID.name}, "
            f"{Editors.JOSM.name}, {Editors.POTLATCH_2.name}, "
            f"{Editors.FIELD_PAPERS.name}, "
            f"{Editors.RAPID.name} "
        )


def is_known_task_creation_mode(value):
    """ Validates Task Creation Mode is known value """
    try:
        TaskCreationMode[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown taskCreationMode: {value} Valid values are {TaskCreationMode.GRID.name}, "
            f"{TaskCreationMode.ARBITRARY.name}"
        )


def is_known_mapping_permission(value):
    """ Validates Mapping Permission String """
    try:
        MappingPermission[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown mappingPermission: {value} Valid values are {MappingPermission.ANY.name}, "
            f"{MappingPermission.LEVEL.name}"
        )


def is_known_validation_permission(value):
    """ Validates Validation Permission String """
    try:
        ValidationPermission[value.upper()]
    except KeyError:
        raise ValidationError(
            f"Unknown validationPermission: {value} Valid values are {ValidationPermission.ANY.name}, "
            f"{ValidationPermission.LEVEL.name}, {ValidationPermission.TEAMS.name}, "
            f"{ValidationPermission.TEAMS_LEVEL.name}"
        )


class DraftProjectDTO(Model):
    """ Describes JSON model used for creating draft project """

    cloneFromProjectId = IntType(serialized_name="cloneFromProjectId")
    project_name = StringType(required=True, serialized_name="projectName")
    organisation = IntType(required=True)
    area_of_interest = BaseType(required=True, serialized_name="areaOfInterest")
    tasks = BaseType(required=False)
    has_arbitrary_tasks = BooleanType(required=True, serialized_name="arbitraryTasks")
    user_id = IntType(required=True)


class ProjectInfoDTO(Model):
    """ Contains the localized project info"""

    locale = StringType(required=True)
    name = StringType(default="")
    short_description = StringType(serialized_name="shortDescription", default="")
    description = StringType(default="")
    instructions = StringType(default="")
    per_task_instructions = StringType(
        default="", serialized_name="perTaskInstructions"
    )


class CustomEditorDTO(Model):
    """ DTO to define a custom editor """

    name = StringType(required=True)
    description = StringType()
    url = StringType(required=True)


class ProjectDTO(Model):
    """ Describes JSON model for a tasking manager project """

    project_id = IntType(serialized_name="projectId")
    project_status = StringType(
        required=True,
        serialized_name="status",
        validators=[is_known_project_status],
        serialize_when_none=False,
    )
    project_priority = StringType(
        required=True,
        serialized_name="projectPriority",
        validators=[is_known_project_priority],
        serialize_when_none=False,
    )
    area_of_interest = BaseType(serialized_name="areaOfInterest")
    aoi_bbox = ListType(FloatType, serialized_name="aoiBBOX")
    tasks = BaseType(serialize_when_none=False)
    default_locale = StringType(
        required=True, serialized_name="defaultLocale", serialize_when_none=False
    )
    project_info = ModelType(
        ProjectInfoDTO, serialized_name="projectInfo", serialize_when_none=False
    )
    project_info_locales = ListType(
        ModelType(ProjectInfoDTO),
        serialized_name="projectInfoLocales",
        serialize_when_none=False,
    )
    mapper_level = StringType(
        required=True,
        serialized_name="mapperLevel",
        validators=[is_known_mapping_level],
    )
    mapping_permission = StringType(
        required=True,
        serialized_name="mappingPermission",
        validators=[is_known_mapping_permission],
    )
    validation_permission = StringType(
        required=True,
        serialized_name="validationPermission",
        validators=[is_known_validation_permission],
    )
    enforce_random_task_selection = BooleanType(
        required=False, default=False, serialized_name="enforceRandomTaskSelection"
    )

    private = BooleanType(required=True)
    changeset_comment = StringType(serialized_name="changesetComment")
    osmcha_filter_id = StringType(serialized_name="osmchaFilterId")
    due_date = UTCDateTimeType(serialized_name="dueDate")
    imagery = StringType()
    josm_preset = StringType(serialized_name="josmPreset", serialize_when_none=False)
    id_presets = ListType(StringType, serialized_name="idPresets", default=[])
    rapid_power_user = BooleanType(
        serialized_name="rapidPowerUser", default=False, required=False
    )
    mapping_types = ListType(
        StringType,
        serialized_name="mappingTypes",
        default=[],
        validators=[is_known_mapping_type],
    )
    campaigns = ListType(ModelType(CampaignDTO), default=[])
    organisation = IntType(required=True)
    organisation_name = StringType(serialized_name="organisationName")
    organisation_slug = StringType(serialized_name="organisationSlug")
    organisation_logo = StringType(serialized_name="organisationLogo")
    country_tag = ListType(StringType, serialized_name="countryTag")

    license_id = IntType(serialized_name="licenseId")
    allowed_usernames = ListType(
        StringType(), serialized_name="allowedUsernames", default=[]
    )
    priority_areas = BaseType(serialized_name="priorityAreas")
    created = UTCDateTimeType()
    last_updated = UTCDateTimeType(serialized_name="lastUpdated")
    author = StringType()
    active_mappers = IntType(serialized_name="activeMappers")
    percent_mapped = IntType(serialized_name="percentMapped")
    percent_validated = IntType(serialized_name="percentValidated")
    percent_bad_imagery = IntType(serialized_name="percentBadImagery")
    task_creation_mode = StringType(
        required=True,
        serialized_name="taskCreationMode",
        validators=[is_known_task_creation_mode],
        serialize_when_none=False,
    )
    project_teams = ListType(ModelType(ProjectTeamDTO), serialized_name="teams")
    mapping_editors = ListType(
        StringType,
        min_size=1,
        required=True,
        serialized_name="mappingEditors",
        validators=[is_known_editor],
    )
    validation_editors = ListType(
        StringType,
        min_size=1,
        required=True,
        serialized_name="validationEditors",
        validators=[is_known_editor],
    )
    custom_editor = ModelType(
        CustomEditorDTO, serialized_name="customEditor", serialize_when_none=False
    )
    interests = ListType(ModelType(InterestDTO))


class ProjectFavoriteDTO(Model):
    """ DTO used to favorite a project """

    project_id = IntType(required=True)
    user_id = IntType(required=True)


class ProjectFavoritesDTO(Model):
    """ DTO to retrieve favorited projects """

    def __init__(self):
        super().__init__()
        self.favorited_projects = []

    favorited_projects = ListType(
        ModelType(ProjectDTO), serialized_name="favoritedProjects"
    )


class ProjectSearchDTO(Model):
    """ Describes the criteria users use to filter active projects"""

    preferred_locale = StringType(default="en")
    mapper_level = StringType(validators=[is_known_mapping_level])
    action = StringType()
    mapping_types = ListType(StringType, validators=[is_known_mapping_type])
    mapping_types_exact = BooleanType(required=False)
    project_statuses = ListType(StringType, validators=[is_known_project_status])
    organisation_name = StringType()
    organisation_id = IntType()
    team_id = IntType()
    campaign = StringType()
    order_by = StringType()
    order_by_type = StringType()
    country = StringType()
    page = IntType(required=True)
    text_search = StringType()
    mapping_editors = ListType(StringType, validators=[is_known_editor])
    validation_editors = ListType(StringType, validators=[is_known_editor])
    teams = ListType(StringType())
    interests = ListType(IntType())
    created_by = IntType(required=False)
    mapped_by = IntType(required=False)
    favorited_by = IntType(required=False)
    managed_by = IntType(required=False)
    omit_map_results = BooleanType(required=False)
    last_updated_lte = StringType(required=False)
    last_updated_gte = StringType(required=False)
    created_lte = StringType(required=False)
    created_gte = StringType(required=False)

    def __hash__(self):
        """ Make object hashable so we can cache user searches"""
        hashable_mapping_types = ""
        if self.mapping_types:
            for mapping_type in self.mapping_types:
                hashable_mapping_types += mapping_type
        hashable_project_statuses = ""
        if self.project_statuses:
            for project_status in self.project_statuses:
                hashable_project_statuses += project_status
        hashable_teams = ""
        if self.teams:
            for team in self.teams:
                hashable_teams += team
        hashable_mapping_editors = ""
        if self.mapping_editors:
            for mapping_editor in self.mapping_editors:
                hashable_mapping_editors = hashable_mapping_editors + mapping_editor
        hashable_validation_editors = ""
        if self.validation_editors:
            for validation_editor in self.validation_editors:
                hashable_validation_editors = (
                    hashable_validation_editors + validation_editor
                )

        return hash(
            (
                self.preferred_locale,
                self.mapper_level,
                hashable_mapping_types,
                hashable_project_statuses,
                hashable_teams,
                self.organisation_name,
                self.campaign,
                self.page,
                self.text_search,
                hashable_mapping_editors,
                hashable_validation_editors,
                self.created_by,
            )
        )


class ProjectSearchBBoxDTO(Model):
    bbox = ListType(FloatType, required=True, min_size=4, max_size=4)
    input_srid = IntType(required=True, choices=[4326])
    preferred_locale = StringType(required=True, default="en")
    project_author = IntType(required=False, serialized_name="projectAuthor")


class ListSearchResultDTO(Model):
    """ Describes one search result"""

    project_id = IntType(required=True, serialized_name="projectId")
    locale = StringType(required=True)
    name = StringType(default="")
    short_description = StringType(serialized_name="shortDescription", default="")
    mapper_level = StringType(required=True, serialized_name="mapperLevel")
    priority = StringType(required=True)
    organisation_name = StringType(serialized_name="organisationName")
    organisation_logo = StringType(serialized_name="organisationLogo")
    campaigns = ListType(ModelType(CampaignDTO), default=[])
    percent_mapped = IntType(serialized_name="percentMapped")
    percent_validated = IntType(serialized_name="percentValidated")
    status = StringType(serialized_name="status")
    active_mappers = IntType(serialized_name="activeMappers")
    last_updated = UTCDateTimeType(serialized_name="lastUpdated")
    due_date = UTCDateTimeType(serialized_name="dueDate")
    total_contributors = IntType(serialized_name="totalContributors")
    country = StringType(serialize_when_none=False)


class ProjectSearchResultsDTO(Model):
    """ Contains all results for the search criteria """

    def __init__(self):
        """ DTO constructor initialise all arrays to empty"""
        super().__init__()
        self.results = []
        self.map_results = []

    map_results = BaseType(serialized_name="mapResults")
    results = ListType(ModelType(ListSearchResultDTO))
    pagination = ModelType(Pagination)


class LockedTasksForUser(Model):
    """ Describes all tasks locked by an individual user"""

    def __init__(self):
        """ DTO constructor initialise all arrays to empty"""
        super().__init__()
        self.locked_tasks = []

    locked_tasks = ListType(IntType, serialized_name="lockedTasks")
    project = IntType(serialized_name="projectId")
    task_status = StringType(serialized_name="taskStatus")


class ProjectComment(Model):
    """ Describes an individual user comment on a project task """

    comment = StringType()
    comment_date = UTCDateTimeType(serialized_name="commentDate")
    user_name = StringType(serialized_name="userName")
    task_id = IntType(serialized_name="taskId")


class ProjectCommentsDTO(Model):
    """ Contains all comments on a project """

    def __init__(self):
        """ DTO constructor initialise all arrays to empty"""
        super().__init__()
        self.comments = []

    comments = ListType(ModelType(ProjectComment))


class ProjectContribDTO(Model):
    date = DateType(required=True)
    mapped = IntType(required=True)
    validated = IntType(required=True)
    cumulative_mapped = IntType(required=False)
    cumulative_validated = IntType(required=False)
    total_tasks = IntType(required=False)


class ProjectContribsDTO(Model):
    """ Contains all contributions on a project by day"""

    def __init__(self):
        """ DTO constructor initialise all arrays to empty"""
        super().__init__()
        self.mapping = []
        self.validation = []

    stats = ListType(ModelType(ProjectContribDTO))


class ProjectSummary(Model):
    """ Model used for PM dashboard """

    project_id = IntType(required=True, serialized_name="projectId")
    default_locale = StringType(serialized_name="defaultLocale")
    author = StringType()
    created = UTCDateTimeType()
    due_date = UTCDateTimeType(serialized_name="dueDate")
    last_updated = UTCDateTimeType(serialized_name="lastUpdated")
    priority = StringType(serialized_name="projectPriority")
    campaigns = ListType(ModelType(CampaignDTO), default=[])
    organisation = IntType()
    organisation_name = StringType(serialized_name="organisationName")
    organisation_slug = StringType(serialized_name="organisationSlug")
    organisation_logo = StringType(serialized_name="organisationLogo")
    country_tag = ListType(StringType, serialized_name="countryTag")
    osmcha_filter_id = StringType(serialized_name="osmchaFilterId")
    mapping_types = ListType(
        StringType, serialized_name="mappingTypes", validators=[is_known_mapping_type]
    )

    changeset_comment = StringType(serialized_name="changesetComment")
    percent_mapped = IntType(serialized_name="percentMapped")
    percent_validated = IntType(serialized_name="percentValidated")
    percent_bad_imagery = IntType(serialized_name="percentBadImagery")
    aoi_centroid = BaseType(serialized_name="aoiCentroid")
    mapper_level = StringType(serialized_name="mapperLevel")
    mapping_permission = IntType(
        serialized_name="mappingPermission", validators=[is_known_mapping_permission]
    )
    validation_permission = IntType(
        serialized_name="validationPermission",
        validators=[is_known_validation_permission],
    )
    allowed_usernames = ListType(
        StringType(), serialized_name="allowedUsernames", default=[]
    )
    random_task_selection_enforced = BooleanType(
        required=False, default=False, serialized_name="enforceRandomTaskSelection"
    )
    private = BooleanType(serialized_name="private")
    allowed_users = ListType(StringType, serialized_name="allowedUsernames", default=[])
    project_teams = ListType(ModelType(ProjectTeamDTO), serialized_name="teams")
    project_info = ModelType(
        ProjectInfoDTO, serialized_name="projectInfo", serialize_when_none=False
    )
    short_description = StringType(serialized_name="shortDescription")
    status = StringType()
    imagery = StringType()
    license_id = IntType(serialized_name="licenseId")
    id_presets = ListType(StringType, serialized_name="idPresets", default=[])
    rapid_power_user = BooleanType(
        serialized_name="rapidPowerUser", default=False, required=False
    )
    mapping_editors = ListType(
        StringType,
        min_size=1,
        required=True,
        serialized_name="mappingEditors",
        validators=[is_known_editor],
    )
    validation_editors = ListType(
        StringType,
        min_size=1,
        required=True,
        serialized_name="validationEditors",
        validators=[is_known_editor],
    )
    custom_editor = ModelType(
        CustomEditorDTO, serialized_name="customEditor", serialize_when_none=False
    )


class PMDashboardDTO(Model):
    """ DTO for constructing the PM Dashboard """

    def __init__(self):
        """ DTO constructor initialise all arrays to empty"""
        super().__init__()
        self.draft_projects = []
        self.archived_projects = []
        self.active_projects = []

    draft_projects = ListType(
        ModelType(ProjectSummary), serialized_name="draftProjects"
    )
    active_projects = ListType(
        ModelType(ProjectSummary), serialized_name="activeProjects"
    )
    archived_projects = ListType(
        ModelType(ProjectSummary), serialized_name="archivedProjects"
    )


class ProjectTaskAnnotationsDTO(Model):
    """ DTO for task annotations of a project """

    def __init__(self):
        """ DTO constructor set task arrays to empty """
        super().__init__()
        self.tasks = []

    project_id = IntType(required=True, serialized_name="projectId")
    tasks = ListType(
        ModelType(TaskAnnotationDTO), required=True, serialized_name="tasks"
    )


class ProjectStatsDTO(Model):
    """ DTO for detailed stats on a project """

    project_id = IntType(required=True, serialized_name="projectId")
    area = FloatType(serialized_name="projectArea(in sq.km)")
    total_mappers = IntType(serialized_name="totalMappers")
    total_tasks = IntType(serialized_name="totalTasks")
    total_comments = IntType(serialized_name="totalComments")
    total_mapping_time = IntType(serialized_name="totalMappingTime")
    total_validation_time = IntType(serialized_name="totalValidationTime")
    total_time_spent = IntType(serialized_name="totalTimeSpent")
    average_mapping_time = IntType(serialized_name="averageMappingTime")
    average_validation_time = IntType(serialized_name="averageValidationTime")
    percent_mapped = IntType(serialized_name="percentMapped")
    percent_validated = IntType(serialized_name="percentValidated")
    percent_bad_imagery = IntType(serialized_name="percentBadImagery")
    aoi_centroid = BaseType(serialized_name="aoiCentroid")
    time_to_finish_mapping = IntType(serialized_name="timeToFinishMapping")
    time_to_finish_validating = IntType(serialized_name="timeToFinishValidating")


class ProjectUserStatsDTO(Model):
    """ DTO for time spent by users on a project """

    time_spent_mapping = IntType(serialized_name="timeSpentMapping")
    time_spent_validating = IntType(serialized_name="timeSpentValidating")
    total_time_spent = IntType(serialized_name="totalTimeSpent")
