import datetime
import ipaddress

from flask import abort, request, current_app
from flask_restful import Resource
from flask_jwt_extended import jwt_required

from zou.app import config
from zou.app.mixin import ArgsMixin
from zou.app.services import (
    persons_service,
    tasks_service,
    time_spents_service,
    shots_service,
    user_service,
)
from zou.app.utils import (
    permissions,
    csv_utils,
    auth,
    fields,
    date_helpers,
    validation,
)
from zou.app.blueprints.persons.schemas import (
    DesktopLoginCreateSchema,
    AddToDepartmentSchema,
    ChangePasswordSchema,
)
from zou.app.services.exception import (
    DepartmentNotFoundException,
    WrongDateFormatException,
    WrongParameterException,
    UnactiveUserException,
    TwoFactorAuthenticationNotEnabledException,
    PersonInProtectedAccounts,
)
from zou.app.services.auth_service import (
    disable_two_factor_authentication_for_person,
)


def _get_project_department_ids_for_person_access(person_id):
    """
    Returns (project_ids, department_ids) for the current user when accessing
    the given person_id. For admin or when person_id is the current user,
    returns (None, None). For manager/supervisor returns (project_ids, department_ids).
    Raises PermissionDenied otherwise.
    """
    if permissions.has_admin_permissions():
        return (None, None)
    current_user_id = persons_service.get_current_user()["id"]
    if current_user_id == person_id:
        return (None, None)
    if not (
        permissions.has_manager_permissions()
        or permissions.has_supervisor_permissions()
    ):
        raise permissions.PermissionDenied
    project_ids = [p["id"] for p in user_service.get_projects()]
    department_ids = None
    if permissions.has_supervisor_permissions():
        department_ids = persons_service.get_current_user(relations=True).get(
            "departments", []
        )
    return (project_ids, department_ids)


class DesktopLoginsResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id):
        """
        Get desktop login logs
        ---
        description: Retrieve desktop login logs for a person. Desktop login
          logs can only be created by current user.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        responses:
          200:
            description: Desktop login logs for the person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                        description: Log entry unique identifier
                        example: a24a6ea4-ce75-4665-a070-57453082c25
                      person_id:
                        type: string
                        format: uuid
                        description: Person unique identifier
                        example: b35b7fb5-df86-5776-b181-68564193d36
                      date:
                        type: string
                        format: date-time
                        description: Login date and time
                        example: "2022-07-12T10:30:00Z"
        """
        current_user = persons_service.get_current_user()
        if (
            current_user["id"] != person_id
            and not permissions.has_manager_permissions()
        ):
            raise permissions.PermissionDenied

        persons_service.get_person(person_id)
        return persons_service.get_desktop_login_logs(person_id)

    @jwt_required()
    def post(self, person_id):
        """
        Create desktop login log
        ---
        description: Add a new log entry for desktop logins for a person.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        requestBody:
          required: true
          content:
            application/json:
              schema:
                type: object
                required:
                  - date
                properties:
                  date:
                    type: string
                    format: date
                    description: Login date
                    example: "2022-07-12"
        responses:
          201:
            description: Desktop login log entry created
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Log entry unique identifier
                      example: a24a6ea4-ce75-4665-a070-57453082c25
                    person_id:
                      type: string
                      format: uuid
                      description: Person unique identifier
                      example: b35b7fb5-df86-5776-b181-68564193d36
                    date:
                      type: string
                      format: date-time
                      description: Login date and time
                      example: "2022-07-12T10:30:00Z"
          400:
            description: Invalid date format
        """
        body = validation.validate_request_body(DesktopLoginCreateSchema)
        date = (
            body.date
            if body.date is not None
            else date_helpers.get_utc_now_datetime()
        )

        current_user = persons_service.get_current_user()
        if (
            current_user["id"] != person_id
            and not permissions.has_admin_permissions()
        ):
            raise permissions.PermissionDenied

        desktop_login_log = persons_service.create_desktop_login_logs(
            person_id, date
        )

        return desktop_login_log, 201


class PresenceLogsResource(Resource):

    @jwt_required()
    def get(self, month_date):
        """
        Get presence logs
        ---
        description: Return a CSV file containing the presence logs based on a
          daily basis for the given month.
        tags:
          - Persons
        parameters:
          - in: path
            name: month_date
            required: true
            schema:
              type: string
              format: date
            description: Month in YYYY-MM format
            example: "2022-07"
        responses:
          200:
            description: CSV file containing the presence logs based on daily basis
            content:
              text/csv:
                schema:
                  type: string
                  format: binary
          400:
            description: Invalid date format
        """
        permissions.check_admin_permissions()
        try:
            date = datetime.datetime.strptime(month_date, "%Y-%m")
        except ValueError:
            raise WrongParameterException("Invalid month or year.")
        presence_logs = persons_service.get_presence_logs(
            date.year, date.month
        )
        return csv_utils.build_csv_response(presence_logs)


class TimeSpentsResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id):
        """
        Get time spents
        ---
        description: Get all time spents for the given person. Optionally can
          accept date range parameters.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: query
            name: start_date
            required: false
            schema:
              type: string
              format: date
            description: Start date for date range filter
            example: "2022-07-01"
          - in: query
            name: end_date
            required: false
            schema:
              type: string
              format: date
            description: End date for date range filter
            example: "2022-07-31"
        responses:
          200:
            description: All time spents for the given person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                        description: Time spent unique identifier
                        example: a24a6ea4-ce75-4665-a070-57453082c25
                      person_id:
                        type: string
                        format: uuid
                        description: Person unique identifier
                        example: b35b7fb5-df86-5776-b181-68564193d36
                      duration:
                        type: number
                        format: float
                        description: Time spent duration in hours
                        example: 8.5
                      date:
                        type: string
                        format: date
                        description: Date of time spent entry
                        example: "2022-07-12"
          400:
            description: Invalid date range parameters
        """
        user_service.check_person_is_not_bot(person_id)
        permissions.check_admin_permissions()
        arguments = self.get_args(["start_date", "end_date"])
        start_date, end_date = arguments["start_date"], arguments["end_date"]
        if not start_date and not end_date:
            return time_spents_service.get_time_spents(person_id)

        if None in [start_date, end_date]:
            abort(
                400,
                "If querying for a range of dates, both a `start_date` and"
                " an `end_date` must be given.",
            )

        try:
            return time_spents_service.get_time_spents_range(
                person_id, start_date, end_date
            )
        except WrongDateFormatException:
            abort(
                400,
                f"Wrong date format for {start_date} and/or {end_date}",
            )


class DateTimeSpentsResource(Resource):

    @jwt_required()
    def get(self, person_id, date):
        """
        Get time spents for date
        ---
        description: Get time spents for given person and specific date.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: date
            required: true
            schema:
              type: string
              format: date
            description: Date to get time spents for
            example: "2022-07-12"
        responses:
          200:
            description: Time spents for given person and date
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                        description: Time spent unique identifier
                        example: a24a6ea4-ce75-4665-a070-57453082c25
                      person_id:
                        type: string
                        format: uuid
                        description: Person unique identifier
                        example: b35b7fb5-df86-5776-b181-68564193d36
                      duration:
                        type: number
                        format: float
                        description: Time spent duration in hours
                        example: 8.5
                      date:
                        type: string
                        format: date
                        description: Date of time spent entry
                        example: "2022-07-12"
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        project_ids, department_ids = (
            _get_project_department_ids_for_person_access(person_id)
        )
        try:
            return time_spents_service.get_time_spents(
                person_id,
                date,
                project_ids=project_ids,
                department_ids=department_ids,
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class DayOffResource(Resource):

    @jwt_required()
    def get(self, person_id, date):
        """
        Get day off
        ---
        description: Get day off object for given person and date.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: date
            required: true
            schema:
              type: string
              format: date
            description: Date to get day off for
            example: "2022-07-12"
        responses:
          200:
            description: Day off object for given person and date
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Day off unique identifier
                      example: a24a6ea4-ce75-4665-a070-57453082c25
                    person_id:
                      type: string
                      format: uuid
                      description: Person unique identifier
                      example: b35b7fb5-df86-5776-b181-68564193d36
                    date:
                      type: string
                      format: date
                      description: Day off date
                      example: "2022-07-12"
                    type:
                      type: string
                      description: Day off type
                      example: "vacation"
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        current_user = persons_service.get_current_user()
        if current_user["id"] != person_id:
            permissions.check_at_least_supervisor_permissions()
        try:
            return time_spents_service.get_day_off(person_id, date)
        except WrongDateFormatException:
            raise WrongParameterException("Invalid date format.")


class PersonDurationTimeSpentsResource(Resource, ArgsMixin):

    def get_project_department_arguments(self, person_id):
        project_id = self.get_project_id()
        project_ids, department_ids = (
            _get_project_department_ids_for_person_access(person_id)
        )
        if project_ids is not None:
            if project_id is None:
                project_id = project_ids
            elif project_id not in project_ids:
                raise permissions.PermissionDenied
        return {
            "project_id": project_id,
            "department_ids": department_ids,
        }


class PersonYearTimeSpentsResource(PersonDurationTimeSpentsResource):

    @jwt_required()
    def get(self, person_id, year):
        """
        Get year time spents
        ---
        description: Get aggregated time spents for given person and year.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get aggregated time spents for
            example: 2022
        responses:
          200:
            description: Aggregated time spents for given person and year
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    total_duration:
                      type: number
                      format: float
                      description: Total duration in hours
                      example: 2080.5
                    year:
                      type: integer
                      description: Year
                      example: 2022
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        try:
            return time_spents_service.get_year_time_spents(
                person_id,
                year,
                **self.get_project_department_arguments(person_id),
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid date format.")


class PersonMonthTimeSpentsResource(PersonDurationTimeSpentsResource):

    @jwt_required()
    def get(self, person_id, year, month):
        """
        Get month time spents
        ---
        description: Get aggregated time spents for given person and month.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get aggregated time spents for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get aggregated time spents for
            example: 7
            minimum: 1
            maximum: 12
        responses:
          200:
            description: Aggregated time spents for given person and month
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    total_duration:
                      type: number
                      format: float
                      description: Total duration in hours
                      example: 173.5
                    year:
                      type: integer
                      description: Year
                      example: 2022
                    month:
                      type: integer
                      description: Month
                      example: 7
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        try:
            return time_spents_service.get_month_time_spents(
                person_id,
                year,
                month,
                **self.get_project_department_arguments(person_id),
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid date format.")


class PersonMonthAllTimeSpentsResource(Resource):

    @jwt_required()
    def get(self, person_id, year, month):
        """
        Get all month time spents
        ---
        description: Get all time spents for a given person and month.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get time spents for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get time spents for
            example: 7
            minimum: 1
            maximum: 12
        responses:
          200:
            description: All time spents for the given person and month
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        user_service.check_person_is_not_bot(person_id)
        user_service.check_person_access(person_id)
        try:
            timespents = time_spents_service.get_time_spents_for_month(
                year, month, person_id=person_id
            )
            return fields.serialize_list(timespents)
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class PersonWeekTimeSpentsResource(PersonDurationTimeSpentsResource):

    @jwt_required()
    def get(self, person_id, year, week):
        """
        Get week time spents
        ---
        description: Get aggregated time spents for given person and week.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get aggregated time spents for
            example: 2022
          - in: path
            name: week
            required: true
            schema:
              type: integer
            description: Week number to get aggregated time spents for
            example: 35
            minimum: 1
            maximum: 52
        responses:
          200:
            description: Aggregated time spents for given person and week
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    total_duration:
                      type: number
                      format: float
                      description: Total duration in hours
                      example: 40.0
                    year:
                      type: integer
                      description: Year
                      example: 2022
                    week:
                      type: integer
                      description: Week number
                      example: 35
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        try:
            return time_spents_service.get_week_time_spents(
                person_id,
                year,
                week,
                **self.get_project_department_arguments(person_id),
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class PersonDayTimeSpentsResource(PersonDurationTimeSpentsResource):

    @jwt_required()
    def get(self, person_id, year, month, day):
        """
        Get day time spents
        ---
        description: Get aggregated time spents for given person and day.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get aggregated time spents for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get aggregated time spents for
            example: 7
            minimum: 1
            maximum: 12
          - in: path
            name: day
            required: true
            schema:
              type: integer
            description: Day to get aggregated time spents for
            example: 12
            minimum: 1
            maximum: 31
        responses:
          200:
            description: Aggregated time spents for given person and day
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    total_duration:
                      type: number
                      format: float
                      description: Total duration in hours
                      example: 8.5
                    year:
                      type: integer
                      description: Year
                      example: 2022
                    month:
                      type: integer
                      description: Month
                      example: 7
                    day:
                      type: integer
                      description: Day
                      example: 12
          400:
            description: Wrong date format
        """
        user_service.check_person_is_not_bot(person_id)
        try:
            return time_spents_service.get_day_time_spents(
                person_id,
                year,
                month,
                day,
                **self.get_project_department_arguments(person_id),
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class PersonQuotaMixin(ArgsMixin):

    def get_quota_arguments(self):
        project_id = self.get_project_id()
        task_type_id = self.get_task_type_id()
        count_mode = self.get_text_parameter("count_mode", default="weighted")
        if count_mode not in ["weighted", "weighteddone", "feedback", "done"]:
            raise WrongParameterException(
                "count_mode must be equal to weighted, weighteddone, feedback"
                ", or done"
            )
        feedback = "done" not in count_mode
        weighted = "weighted" in count_mode

        return (project_id, task_type_id, feedback, weighted)

    def check_permissions(self, person_id, project_id=None):
        if permissions.has_manager_permissions():
            user_service.check_manager_project_access(project_id)
        else:
            user_service.check_person_access(person_id)

    def get_person_quotas(self):
        pass

    @jwt_required()
    def get(self, person_id, *args, **kwargs):
        user_service.check_person_is_not_bot(person_id)
        (project_id, task_type_id, feedback, weighted) = (
            self.get_quota_arguments()
        )
        self.check_permissions(person_id, project_id)

        try:
            return self.get_person_quotas(
                person_id,
                *args,
                **kwargs,
                project_id=project_id,
                task_type_id=task_type_id,
                feedback=feedback,
                weighted=weighted,
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class PersonMonthQuotaShotsResource(Resource, PersonQuotaMixin):

    def get_person_quotas(self, person_id, year, month, **kwargs):
        return shots_service.get_month_quota_shots(
            person_id, year, month, **kwargs
        )

    @jwt_required()
    def get(self, person_id, year, month):
        """
        Get month quota shots
        ---
        description: Get ended shots used for quota calculation of this month.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get quota shots for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get quota shots for
            example: 7
            minimum: 1
            maximum: 12
          - in: query
            name: count_mode
            required: false
            schema:
              type: string
              enum: [weighted, weighteddone, feedback, done]
            description: Count mode for quota calculation
            example: weighted
        responses:
          200:
            description: Ended shots used for quota calculation of this month
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
          400:
            description: Wrong date format or invalid count mode
        """
        return super().get(person_id, year, month)


class PersonWeekQuotaShotsResource(Resource, PersonQuotaMixin):

    def get_person_quotas(self, person_id, year, week, **kwargs):
        return shots_service.get_week_quota_shots(
            person_id, year, week, **kwargs
        )

    @jwt_required()
    def get(self, person_id, year, week):
        """
        Get week quota shots
        ---
        description: Get ended shots used for quota calculation of this week.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get quota shots for
            example: 2022
          - in: path
            name: week
            required: true
            schema:
              type: integer
            description: Week number to get quota shots for
            example: 35
            minimum: 1
            maximum: 52
          - in: query
            name: count_mode
            required: false
            schema:
              type: string
              enum: [weighted, weighteddone, feedback, done]
            description: Count mode for quota calculation
            example: weighted
        responses:
          200:
            description: Ended shots used for quota calculation of this week
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
          400:
            description: Wrong date format or invalid count mode
        """
        return super().get(person_id, year, week)


class PersonDayQuotaShotsResource(Resource, PersonQuotaMixin):

    def get_person_quotas(self, person_id, year, month, day, **kwargs):
        return shots_service.get_day_quota_shots(
            person_id, year, month, day, **kwargs
        )

    @jwt_required()
    def get(self, person_id, year, month, day):
        """
        Get day quota shots
        ---
        description: Get ended shots used for quota calculation of this day.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get quota shots for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get quota shots for
            example: 7
            minimum: 1
            maximum: 12
          - in: path
            name: day
            required: true
            schema:
              type: integer
            description: Day to get quota shots for
            example: 12
            minimum: 1
            maximum: 31
          - in: query
            name: count_mode
            required: false
            schema:
              type: string
              enum: [weighted, weighteddone, feedback, done]
            description: Count mode for quota calculation
            example: weighted
        responses:
          200:
            description: Ended shots used for quota calculation of this day
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
          400:
            description: Wrong date format or invalid count mode
        """
        return super().get(person_id, year, month, day)


class TimeSpentDurationResource(Resource, ArgsMixin):
    """
    Parent class for all durations time spents resource.
    """

    def get_person_project_department_arguments(self):
        project_id = self.get_project_id()
        person_id = None
        department_id = self.get_text_parameter("department_id")
        if department_id is not None:
            department_ids = [department_id]
        else:
            department_ids = None
        studio_id = self.get_text_parameter("studio_id")
        if not permissions.has_admin_permissions():
            if (
                permissions.has_manager_permissions()
                or permissions.has_supervisor_permissions()
            ):
                project_ids = [
                    project["id"] for project in user_service.get_projects()
                ]
                if project_id is None:
                    project_id = project_ids
                elif project_id not in project_ids:
                    raise permissions.PermissionDenied
                if permissions.has_supervisor_permissions():
                    persons_departments = persons_service.get_current_user(
                        relations=True
                    )["departments"]
                    if department_id is not None:
                        if department_id not in persons_departments:
                            raise WrongParameterException(
                                "Supervisor not allowed to access this department"
                            )
                    else:
                        department_ids = persons_departments
            else:
                person_id = persons_service.get_current_user()["id"]

        return {
            "person_id": person_id,
            "project_id": project_id,
            "department_ids": department_ids,
            "studio_id": studio_id,
        }


class TimeSpentMonthResource(TimeSpentDurationResource):

    @jwt_required()
    def get(self, year, month):
        """
        Get time spent month table
        ---
        description: Return a table giving time spent by user and by day for
          given year and month.
        tags:
          - Persons
        parameters:
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get time spent table for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get time spent table for
            example: 7
            minimum: 1
            maximum: 12
        responses:
          200:
            description: Table giving time spent by user and by day for given year and month
            content:
              application/json:
                schema:
                  type: object
        """
        try:
            return time_spents_service.get_day_table(
                year, month, **self.get_person_project_department_arguments()
            )
        except WrongDateFormatException:
            raise WrongParameterException("Invalid month or year.")


class TimeSpentYearsResource(TimeSpentDurationResource):

    @jwt_required()
    def get(self):
        """
        Get time spent years table
        ---
        description: Return a table giving time spent by user and by month for
          all years.
        tags:
          - Persons
        responses:
          200:
            description: Table giving time spent by user and by month for all years
            content:
              application/json:
                schema:
                  type: object
        """
        return time_spents_service.get_year_table(
            **self.get_person_project_department_arguments()
        )


class TimeSpentMonthsResource(TimeSpentDurationResource):

    @jwt_required()
    def get(self, year):
        """
        Get time spent months table
        ---
        description: Return a table giving time spent by user and by month for
          given year.
        tags:
          - Persons
        parameters:
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get time spent table for
            example: 2022
        responses:
          200:
            description: Table giving time spent by user and by month for given year
            content:
              application/json:
                schema:
                  type: object
        """
        return time_spents_service.get_month_table(
            year, **self.get_person_project_department_arguments()
        )


class TimeSpentWeekResource(TimeSpentDurationResource):

    @jwt_required()
    def get(self, year):
        """
        Get time spent weeks table
        ---
        description: Return a table giving time spent by user and by week for
          given year.
        tags:
          - Persons
        parameters:
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get time spent table for
            example: 2022
        responses:
          200:
            description: Table giving time spent by user and by week for given year
            content:
              application/json:
                schema:
                  type: object
        """
        return time_spents_service.get_week_table(
            year, **self.get_person_project_department_arguments()
        )


class InvitePersonResource(Resource):

    @jwt_required()
    def get(self, person_id):
        """
        Invite person
        ---
        description: Sends an email to given person to invite him or her to
          connect to Kitsu.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        responses:
          200:
            description: Email sent successfully
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    success:
                      type: boolean
                      description: Success flag
                      example: true
                    message:
                      type: string
                      description: Success message
                      example: "Email sent"
        """
        user_service.check_person_is_not_bot(person_id)
        permissions.check_admin_permissions()
        persons_service.invite_person(person_id)
        return {"success": True, "message": "Email sent"}


class DayOffForMonthResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, year, month):
        """
        Get day offs for month
        ---
        description: Return all day off recorded for given month. Admins get all
          day offs, regular users get only their own.
        tags:
          - Persons
        parameters:
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get day offs for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get day offs for
            example: 7
            minimum: 1
            maximum: 12
        responses:
          200:
            description: All day off recorded for given month
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        if permissions.has_admin_permissions():
            return time_spents_service.get_day_offs_for_month(year, month)
        else:
            person_id = persons_service.get_current_user()["id"]
            return time_spents_service.get_person_day_offs_for_month(
                person_id, year, month
            )


class PersonWeekDayOffResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id, year, week):
        """
        Get person week day offs
        ---
        description: Return all day off recorded for given week and person.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get day offs for
            example: 2022
          - in: path
            name: week
            required: true
            schema:
              type: integer
            description: Week number to get day offs for
            example: 35
            minimum: 1
            maximum: 52
        responses:
          200:
            description: All day off recorded for given week and person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        user_service.check_person_is_not_bot(person_id)
        user_service.check_person_access(person_id)
        return time_spents_service.get_person_day_offs_for_week(
            person_id, year, week
        )


class PersonMonthDayOffResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id, year, month):
        """
        Get person month day offs
        ---
        description: Return all day off recorded for given month and person.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get day offs for
            example: 2022
          - in: path
            name: month
            required: true
            schema:
              type: integer
            description: Month to get day offs for
            example: 7
            minimum: 1
            maximum: 12
        responses:
          200:
            description: All day off recorded for given month and person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        user_service.check_person_is_not_bot(person_id)
        user_service.check_person_access(person_id)
        return time_spents_service.get_person_day_offs_for_month(
            person_id, year, month
        )


class PersonYearDayOffResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id, year):
        """
        Get person year day offs
        ---
        description: Return all day off recorded for given year and person.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: year
            required: true
            schema:
              type: integer
            description: Year to get day offs for
            example: 2022
        responses:
          200:
            description: All day off recorded for given year and person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        user_service.check_person_is_not_bot(person_id)
        user_service.check_person_access(person_id)
        return time_spents_service.get_person_day_offs_for_year(
            person_id, year
        )


class PersonDayOffResource(Resource, ArgsMixin):

    @jwt_required()
    def get(self, person_id):
        """
        Get person day offs
        ---
        description: Return all day offs recorded for given person.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        responses:
          200:
            description: All day off recorded for given person
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
        """
        user_service.check_person_is_not_bot(person_id)
        user_service.check_person_access(person_id)
        return time_spents_service.get_day_offs_between(
            person_id=person_id,
        )


class AddToDepartmentResource(Resource, ArgsMixin):

    @jwt_required()
    def post(self, person_id):
        """
        Add person to department
        ---
        description: Add a user to given department.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        requestBody:
          required: true
          content:
            application/json:
              schema:
                type: object
                required:
                  - department_id
                properties:
                  department_id:
                    type: string
                    format: uuid
                    description: Department unique identifier
                    example: b35b7fb5-df86-5776-b181-68564193d36
        responses:
          201:
            description: User added to given department
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Person unique identifier
                      example: a24a6ea4-ce75-4665-a070-57453082c25
                    department_id:
                      type: string
                      format: uuid
                      description: Department unique identifier
                      example: b35b7fb5-df86-5776-b181-68564193d36
          400:
            description: Invalid department ID
        """
        permissions.check_admin_permissions()
        body = validation.validate_request_body(AddToDepartmentSchema)
        department_id = str(body.department_id)

        try:
            department = tasks_service.get_department(department_id)
        except DepartmentNotFoundException:
            raise WrongParameterException(
                "Department ID matches no department"
            )
        person = persons_service.add_to_department(department["id"], person_id)
        return person, 201


class RemoveFromDepartmentResource(Resource, ArgsMixin):

    @jwt_required()
    def delete(self, person_id, department_id):
        """
        Remove person from department
        ---
        description: Remove a user from given department.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
          - in: path
            name: department_id
            required: true
            schema:
              type: string
              format: uuid
            description: Department unique identifier
            example: b35b7fb5-df86-5776-b181-68564193d36
        responses:
          204:
            description: User removed from given department
        """
        permissions.check_admin_permissions()
        try:
            tasks_service.get_department(department_id)
        except DepartmentNotFoundException:
            raise WrongParameterException(
                "Department ID matches no department"
            )
        persons_service.remove_from_department(department_id, person_id)
        return "", 204


class ChangePasswordForPersonResource(Resource, ArgsMixin):

    @jwt_required()
    def post(self, person_id):
        """
        Change person password
        ---
        description: Allow admin to change password for given user.
          An admin can't change other admins password. The new password requires
          a confirmation to ensure that the admin didn't make a mistake by
          typing the new password.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        requestBody:
          required: true
          content:
            application/json:
              schema:
                type: object
                required:
                  - password
                  - password_2
                properties:
                  password:
                    type: string
                    format: password
                    description: New password
                    example: "newSecurePassword123"
                  password_2:
                    type: string
                    format: password
                    description: Password confirmation
                    example: "newSecurePassword123"
        responses:
          200:
            description: Password changed successfully
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    success:
                      type: boolean
                      description: Success flag
                      example: true
          400:
            description: Invalid password or inactive user
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    error:
                      type: boolean
                      description: Error flag
                      example: true
                    message:
                      type: string
                      description: Error message
                      example: "Password is too short."
        """
        user_service.check_person_is_not_bot(person_id)
        permissions.check_admin_permissions()
        body = validation.validate_request_body(ChangePasswordSchema)
        password, password_2 = body.password, body.password_2
        current_user = persons_service.get_current_user()
        try:
            person = persons_service.get_person(person_id)
            if person["id"] != current_user["id"] and (
                person["email"] in config.PROTECTED_ACCOUNTS
                or person["role"] == "admin"
            ):
                raise PersonInProtectedAccounts()
            auth.validate_password(password, password_2)
            password = auth.encrypt_password(password)
            persons_service.update_password(person["email"], password)
            current_app.logger.warning(
                "User %s has changed the password of %s"
                % (current_user["email"], person["email"])
            )
            person_IP = request.headers.get("X-Forwarded-For", None)
            if person_IP:
                try:
                    ipaddress.ip_address(person_IP.split(",")[0].strip())
                    person_IP = person_IP.split(",")[0].strip()
                except ValueError:
                    person_IP = None
            persons_service.send_password_changed_by_admin_email(
                person, current_user, person_IP=person_IP
            )
            return {"success": True}

        except auth.PasswordsNoMatchException:
            return (
                {
                    "error": True,
                    "message": "Confirmation password doesn't match.",
                },
                400,
            )
        except auth.PasswordTooShortException:
            return {"error": True, "message": "Password is too short."}, 400
        except UnactiveUserException:
            return {"error": True, "message": "User is unactive."}, 400
        except PersonInProtectedAccounts:
            return (
                {
                    "error": True,
                    "message": "This user is in protected accounts.",
                },
                400,
            )


class DisableTwoFactorAuthenticationPersonResource(Resource, ArgsMixin):

    @jwt_required()
    def delete(self, person_id):
        """
        Disable two factor authentication
        ---
        description: Allow admin to disable two factor authentication for given
          user. An admin can't disable two factor authentication for other
          admins.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        responses:
          200:
            description: Two factor authentication disabled successfully
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    success:
                      type: boolean
                      description: Success flag
                      example: true
          400:
            description: Inactive user or two factor authentication not enabled
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    error:
                      type: boolean
                      description: Error flag
                      example: true
                    message:
                      type: string
                      description: Error message
                      example: "User is unactive."
        """
        user_service.check_person_is_not_bot(person_id)
        permissions.check_admin_permissions()
        current_user = persons_service.get_current_user()
        try:
            person = persons_service.get_person(person_id)
            if (
                person["role"] == "admin"
                and person["id"] != current_user["id"]
            ):
                return {
                    "error": True,
                    "message": "An admin can't disable 2FA for other admins.",
                }, 400
            disable_two_factor_authentication_for_person(person["id"])
            current_app.logger.warning(
                "User %s has disabled the two factor authentication of %s"
                % (current_user["email"], person["email"])
            )
            person_IP = request.headers.get("X-Forwarded-For", None)
            if person_IP:
                try:
                    ipaddress.ip_address(person_IP.split(",")[0].strip())
                    person_IP = person_IP.split(",")[0].strip()
                except ValueError:
                    person_IP = None
            persons_service.send_2fa_disabled_by_admin_email(
                person, current_user, person_IP=person_IP
            )
            return {"success": True}

        except UnactiveUserException:
            return {"error": True, "message": "User is unactive."}, 400
        except TwoFactorAuthenticationNotEnabledException:
            return {
                "error": True,
                "message": "Two factor authentication not enabled for this user.",
            }, 400


class ClearAvatarPersonResource(Resource):
    @jwt_required()
    def delete(self, person_id):
        """
        Clear person avatar
        ---
        description: Set has_avatar flag to False for current user and remove
          its avatar file.
        tags:
          - Persons
        parameters:
          - in: path
            name: person_id
            required: true
            schema:
              type: string
              format: uuid
            description: Person unique identifier
            example: a24a6ea4-ce75-4665-a070-57453082c25
        responses:
          204:
            description: Avatar file deleted
        """
        permissions.check_admin_permissions()
        persons_service.clear_avatar(person_id)
        return "", 204
