import logging
from datetime import datetime
from typing import List

from django.conf import settings

from staff.payment.enums import WAGE_SYSTEM

from staff.lib import requests
from staff.lib.json import dumps as json_dumps
from staff.lib.tvm2 import TVM_SERVICE_TICKET_HEADER, get_tvm_ticket_by_deploy

from staff.person.models import Organization
from staff.budget_position.const import PUSH_STATUS, WORKFLOW_STATUS
from staff.budget_position.workflow_service import entities, use_cases
from staff.budget_position.workflow_service.gateways.errors import OebsHireError

logger = logging.getLogger(__name__)


class OebsHireService(use_cases.OebsHireService):
    """
    Сервис Я.Найм, сервис-обертка над связанным с наймом функционалом oebs
    """
    def push_workflow(self, workflow: entities.AbstractWorkflow) -> None:
        if workflow.status != WORKFLOW_STATUS.PUSHED:
            raise entities.WorkflowInvalidStateError(workflow.id, workflow.status)
        self.send_change(workflow.changes[0])
        workflow.mark_pushed_to_oebs()

    def send_change(self, change: entities.Change) -> None:
        data = self._make_data_for_change(change)

        try:
            result = self._try_send(data, change.id)
        except (ValueError, requests.HTTPError, requests.Timeout) as e:
            change.push_status = PUSH_STATUS.ERROR
            msg = f'OEBS Hire did not answer: {e}'
            logger.error(msg)
            raise OebsHireError(description=msg)

        assignment_id = result.get('id')
        if assignment_id and assignment_id > 0:
            change.sent_to_oebs = datetime.now()
            change.push_status = PUSH_STATUS.FINISHED
            change.assignment_id = assignment_id
            return
        self._raise_for_bad_assignment_id(assignment_id, result.get('errors', []), change)

    def _try_send(self, data: dict, change_id: int):
        response = requests.post(
            url=settings.OEBS_ASSIGNMENT_CREATE_URL,
            headers={
                TVM_SERVICE_TICKET_HEADER: get_tvm_ticket_by_deploy('oebs-api'),
                'Content-Type': 'application/json',
            },
            log_message=f'Sending change ({change_id}) to OEBS Hire',
            # Note: делаем один большой таймаут. Если можно будет ретраиться в этой ручке,
            # стоит переделать на несколько
            timeout=10,
            # With `data` arg we avoid private data logging (e.g. salary)
            data=json_dumps(data),
        )
        response.raise_for_status()
        return response.json()

    def _raise_for_bad_assignment_id(self, assignment_id: int, errors: List, change: entities.Change):
        description = ''
        if assignment_id is None:
            description = 'OEBS Hire did not return assignment id'
        elif assignment_id == 0:
            description = 'OEBS Hire validation error'
        elif assignment_id < 0:
            description = 'OEBS system error'
        logger.error(
            'Error on creating assignment in OEBS Hire: %s. OEBS errors: %s',
            description, errors,
        )
        change.push_status = PUSH_STATUS.ERROR
        raise OebsHireError(description=description, oebs_internal_errors=errors)

    def _make_data_for_change(self, change: entities.Change) -> dict:
        organization = (
            Organization.objects
            .filter(id=change.organization_id)
            .select_related('oebs_organization')
            .first()
        )
        # Связанная организация обязана быть, если ее нет, то с синком что-то не так
        oebs_organization = organization and organization.oebs_organization

        optional_data = {
            # Номер БП
            'positionNum': change.budget_position and change.budget_position.code,
            # ID расположения Oracle
            'officeId': change.placement_id,
            # ID юридического лица Oracle
            'taxUnitId': oebs_organization and oebs_organization.org_id,
            # ID подразделения Oracle
            'departmentId': change.department_id,
            # ID должности Oracle
            'jobId': change.position_id,
            # Форма занятости
            'peopleGroupId': self._get_people_group_id(change.employment_type),
            # Базис оклада
            'base': self._get_wage_base(change.currency, change.wage_system),
            # OEBS ждет в json тип float, несмотря на то, что это зарплата
            #   поэтому мы не можем оставлять тут Decimal, он сконвертируется в строку,
            #   которую OEBS не примет
            'salary': float(change.salary) if (change.salary is not None) else None,
            # Сумма доплаты за интернет
            'otherPay': change.other_payments,
            # Код испытательного срока
            'probPeriod': change.probation_period_code,
            'replacement': change.is_replacement,
            # Логин сотрудника на замену. Только для замен по причине декретного отпуска.
            # Замена по увольнению/ротации - это для OEBS обычный найм без связки с уходящим сотрудником.
            'insteadOf': change.instead_of_login,
            # Дата окончания срочного договора
            'dateEnd': change.contract_term_date and change.contract_term_date.isoformat(),
            # Продолжительность срочного договора
            'contractPeriod': change.contract_period,
        }
        data = {
            key: value
            for key, value in optional_data.items()
            if value or value == 0
        }
        # ID физ. лица Oracle
        data['personId'] = change.physical_entity_id
        data['dateHire'] = change.join_at.isoformat()
        return data

    def _get_people_group_id(self, employment_type):
        if not employment_type:
            return None
        if employment_type == 'full':
            return 10
        return 11

    def _get_wage_base(self, currency: str, wage_system: str):
        if not currency or not wage_system:
            return None
        wage_system_translation = {
            WAGE_SYSTEM.PIECEWORK: 'сдельная оплата',
            WAGE_SYSTEM.HOURLY: 'часовая ставка',
            WAGE_SYSTEM.FIXED: 'месячный оклад',
        }.get(wage_system, wage_system)
        return f'{currency} ({wage_system_translation})'
