<?php
/**
 * Created by PhpStorm.
 * User: Thilina
 * Date: 8/19/17
 * Time: 4:18 PM
 */

namespace Payroll\Admin\Api;

use Classes\BaseService;
use Classes\IceResponse;
use Classes\SubActionManager;
use Company\Common\Model\CompanyStructure;
use Employees\Common\Model\Employee;
use Payroll\Common\Model\Deduction;
use Payroll\Common\Model\DeductionGroup;
use Payroll\Common\Model\Payroll;
use Payroll\Common\Model\PayrollCalculations;
use Payroll\Common\Model\PayrollColumn;
use Payroll\Common\Model\PayrollData;
use Salary\Common\Model\EmployeeSalary;
use Salary\Common\Model\PayrollEmployee;
use Salary\Common\Model\SalaryComponent;
use Utils\LogManager;
use Utils\Math\EvalMath;
use Utils\ScriptRunner;
use Classes\Migration\AbstractMigration;

class PayrollActionManager extends SubActionManager
{

    const REG_HOURS_PER_WEEK = 40;

    protected $calCache = array();

    public function addToCalculationCache($key, $val)
    {
        $this->calCache[$key] = $val;
    }

    public function getFromCalculationCache($key)
    {
        if (isset($this->calCache[$key])) {
            return $this->calCache[$key];
        }

        return null;
    }

    public function calculatePayrollColumn(
        $col,
        $payroll,
        $employeeId,
        $payrollEmployeeId,
        $noColumnCalculations = false
    ) {

        $val = $this->getFromCalculationCache($col->id."-".$payroll->id."-".$employeeId);
        if (!empty($val)) {
            return $val;
        }

        if (!empty($col->calculation_hook)) {
            $valueData = BaseService::getInstance()->executeCalculationHook(
                array($employeeId, $payroll->date_start, $payroll->date_end),
                $col->calculation_hook,
                $col->calculation_function
            );
            if (is_array($valueData) && $valueData[0] == 'string') {
                $val = $valueData[1];
            } else {
                $val = number_format(round($valueData, 2), 2, '.', '');
            }

            $this->addToCalculationCache($col->id."-".$payroll->id."-".$employeeId, $val);
            return $val;
        }

        $sum = 0;

        $payRollEmp = new PayrollEmployee();
        $payRollEmp->Load("id = ?", array($payrollEmployeeId));

        //Salary
        LogManager::getInstance()->info("salary components row:".$col->salary_components);
        if (!empty($col->salary_components) &&
            !empty(json_decode($col->salary_components, true))) {
            $salaryComponent = new SalaryComponent();
            $salaryComponents = $salaryComponent->Find(
                "id in (".implode(",", json_decode($col->salary_components, true)).")",
                array()
            );
            LogManager::getInstance()->info("salary components:".$salaryComponents);
            foreach ($salaryComponents as $salaryComponent) {
                $sum += $this->getTotalForEmployeeSalaryByComponent($employeeId, $salaryComponent->id);
            }
        }

        //Deductions
        if (!empty($col->deductions) &&
            !empty(json_decode($col->deductions, true))) {
            $deduction = new Deduction();
            if (empty($payRollEmp->deduction_group)) {
                $deductions = $deduction->Find(
                    "id in (".implode(",", json_decode($col->deductions, true)).")",
                    array()
                );
            } else {
                $deductions = $deduction->Find(
                    "deduction_group = ? and id in (".implode(",", json_decode($col->deductions, true)).")",
                    array($payRollEmp->deduction_group)
                );
            }

            $allowedDeductions = $this->getAllowedDeductionsForEmployee($employeeId, $payRollEmp->deduction_group);
            foreach ($deductions as $deduction) {
                if (!in_array($deduction->id, $allowedDeductions)) {
                    continue;
                }
            }
            foreach ($deductions as $deduction) {
                $sum += $this->calculateDeductionValue($employeeId, $deduction, $payroll);
            }
        }

        if (!$noColumnCalculations) {
            $evalMath = new EvalMath();
            if ($col->function_type === 'Simple') {
                $evalMath->evaluate('max(x,y) = (y - x) * ceil(tanh(exp(tanh(y - x)) - exp(0))) + x');
                $evalMath->evaluate('min(x,y) = y - (y - x) * ceil(tanh(exp(tanh(y - x)) - exp(0)))');
            }

            if (!empty($col->add_columns) &&
                !empty(json_decode($col->add_columns, true))) {
                $colIds = json_decode($col->add_columns, true);
                $payrollColumn = new PayrollColumn();
                $payrollColumns = $payrollColumn->Find("id in (".implode(",", $colIds).")", array());
                foreach ($payrollColumns as $payrollColumn) {
                    $sum += $this->calculatePayrollColumn(
                        $payrollColumn,
                        $payroll,
                        $employeeId,
                        $payrollEmployeeId,
                        true
                    );
                }
            }

            if (!empty($col->sub_columns) &&
                !empty(json_decode($col->sub_columns, true))) {
                $colIds = json_decode($col->sub_columns, true);
                $payrollColumn = new PayrollColumn();
                $payrollColumns = $payrollColumn->Find("id in (".implode(",", $colIds).")", array());
                foreach ($payrollColumns as $payrollColumn) {
                    $sum -= $this->calculatePayrollColumn(
                        $payrollColumn,
                        $payroll,
                        $employeeId,
                        $payrollEmployeeId,
                        true
                    );
                }
            }

            if (!empty($col->calculation_columns) &&
                !empty(json_decode($col->calculation_columns, true)) && !empty($col->calculation_function)) {
                $cc = json_decode($col->calculation_columns);
                $func = $col->calculation_function;
                $variableList = [];
                foreach ($cc as $c) {
                    $value = $this->getFromCalculationCache($c->column."-".$payroll->id."-".$employeeId);
                    if ($col->function_type === 'Simple') {
                        if (empty($value)) {
                            $value = 0.00;
                        }
                        $func = str_replace($c->name, $value, $func);
                    } else {
                        $variableList[$c->name] = $value;
                    }
                }
                try {
                    if ($col->function_type === 'Simple') {
                        $sum += $evalMath->evaluate($func);
                    } else {
                        $sum = ScriptRunner::executeJs($variableList, $func);
                    }
                } catch (\Exception $e) {
                    LogManager::getInstance()->error("Error:".$e->getMessage());
                    LogManager::getInstance()->notifyException($e);
                }
            }
        }

        //return $sum;
        $val = number_format(round($sum, 2), 2, '.', '');
        $this->addToCalculationCache($col->id."-".$payroll->id."-".$employeeId, $val);
        return $val;
    }

    private function calculateDeductionValue($employeeId, $deduction, $payroll)
    {

        $salaryComponents = array();
        if (!empty($deduction->componentType) && !empty(json_decode($deduction->componentType, true))) {
            $salaryComponent = new SalaryComponent();
            $salaryComponents = $salaryComponent->Find(
                "componentType in (".implode(",", json_decode($deduction->componentType, true)).")",
                array()
            );
        }

        $salaryComponents2 = array();
        if (!empty($deduction->component) && !empty(json_decode($deduction->component, true))) {
            $salaryComponent = new SalaryComponent();
            $salaryComponents2 = $salaryComponent->Find(
                "id in (".implode(",", json_decode($deduction->component, true)).")",
                array()
            );
        }

        $sum = 0;

        $salaryComponentIDs = array();
        foreach ($salaryComponents as $sc) {
            $salaryComponentIDs[] = $sc->id;
            $sum += $this->getTotalForEmployeeSalaryByComponent($employeeId, $sc->id);
        }

        foreach ($salaryComponents2 as $sc) {
            if (!in_array($sc->id, $salaryComponentIDs)) {
                $salaryComponents[] = $sc;
                $sum += $this->getTotalForEmployeeSalaryByComponent($employeeId, $sc->id);
            }
        }

        if (!empty($deduction->payrollColumn)) {
            $columnVal = $this->getFromCalculationCache($deduction->payrollColumn."-".$payroll->id."-".$employeeId);
            if (!empty($columnVal)) {
                $sum += $columnVal;
            }
        }

        $deductionFunction = $this->getDeductionFunction($deduction, $sum);
        if (empty($deductionFunction)) {
            LogManager::getInstance()->error("Deduction function not found");
            return 0;
        }

        $deductionFunction = str_replace("X", $sum, $deductionFunction);

        $evalMath = new EvalMath();
        $val = $evalMath->evaluate($deductionFunction);
        return floatval($val);
    }

    private function getDeductionFunction($deduction, $amount)
    {
        $amount = floatval($amount);
        $ranges = json_decode($deduction->rangeAmounts);
        foreach ($ranges as $range) {
            $lowerLimitPassed = false;
            if ($range->lowerCondition == "No Lower Limit") {
                $lowerLimitPassed = true;
            } elseif ($range->lowerCondition == "gt") {
                if (floatval($range->lowerLimit) < $amount) {
                    $lowerLimitPassed = true;
                }
            } elseif ($range->lowerCondition == "gte") {
                if (floatval($range->lowerLimit) <= $amount) {
                    $lowerLimitPassed = true;
                }
            }

            $upperLimitPassed = false;
            if ($range->upperCondition == "No Upper Limit") {
                $upperLimitPassed = true;
            } elseif ($range->upperCondition == "lt") {
                if (floatval($range->upperLimit) > $amount) {
                    $upperLimitPassed = true;
                }
            } elseif ($range->upperCondition == "lte") {
                if (floatval($range->upperLimit) >= $amount) {
                    $upperLimitPassed = true;
                }
            }

            if ($lowerLimitPassed && $upperLimitPassed) {
                return $range->amount;
            }
        }
        return null;
    }

    private function getTotalForEmployeeSalaryByComponent($employeeId, $salaryComponentId)
    {
        $empSalary = new EmployeeSalary();
        $list = $empSalary->Find("employee = ? and component =?", array($employeeId, $salaryComponentId));
        $sum = 0;
        foreach ($list as $empSalary) {
            $sum += floatval($empSalary->amount);
        }

        return $sum;
    }

    private function getAllowedDeductionsForEmployee($employeeId, $deductionGroup)
    {
        $payrollEmp = new PayrollEmployee();
        $payrollEmp->Load("employee = ?", array($employeeId));

        $allowed = array();
        $deduction = new Deduction();
        if (empty($payrollEmp->deduction_allowed) || empty(json_decode($payrollEmp->deduction_allowed, true))) {
            $allowed =  $deduction->Find("deduction_group = ?", array($deductionGroup));
        } else {
            $allowedIds = json_decode($payrollEmp->deduction_allowed, true);
            $allowed =  $deduction->Find("id in (".implode(",", $allowedIds).")");
        }

        $allowedFiltered = array();
        $disallowedIds = json_decode($payrollEmp->deduction_exemptions, true);

        if (!empty($disallowedIds)) {
            foreach ($allowed as $item) {
                if (!in_array($item->id, $disallowedIds)) {
                    $allowedFiltered[] = $item;
                }
            }
        } else {
            $allowedFiltered = $allowed;
        }

        $allowedIds = array();
        foreach ($allowedFiltered as $item) {
            $allowedIds[] = $item->id;
        }

        return $allowedIds;
    }

    public function getAllData($req)
    {

        $cal = new PayrollCalculations();

        $rowTable = BaseService::getInstance()->getFullQualifiedModelClassName($req->rowTable);
        $columnTable = BaseService::getInstance()->getFullQualifiedModelClassName($req->columnTable);
        $valueTable = BaseService::getInstance()->getFullQualifiedModelClassName($req->valueTable);
        $save = $req->save;

        //Only select employees matching pay frequency

        $payroll = new Payroll();
        $payroll->Load("id = ?", array($req->payrollId));
        $columnList = json_decode($payroll->columns, true);

        //Get Child company structures
        $cssResp = CompanyStructure::getAllChildCompanyStructures($payroll->department);
        error_log(json_encode($cssResp));
        $css = $cssResp->getData();
        $cssIds = array();
        foreach ($css as $c) {
            $cssIds[] = $c->id;
        }

        $employeeNamesById = array();
        $baseEmp = new Employee();
        $baseEmpList = $baseEmp->Find(
            "department in (".implode(",", $cssIds).") and status = ?",
            array('Active')
        );
        $empIds = array();
        foreach ($baseEmpList as $baseEmp) {
            $employeeNamesById[$baseEmp->id] = $baseEmp->first_name." ".$baseEmp->last_name;
            $empIds[] = $baseEmp->id;
        }

        $emp = new $rowTable();
        $emps = $emp->Find(
            "pay_frequency = ? and deduction_group = ? and employee in (".implode(",", $empIds).")",
            array($payroll->pay_period, $payroll->deduction_group)
        );
        if (!$emps) {
            error_log("Error:".$emp->ErrorMsg());
        } else {
            error_log("Employees:".json_encode($emps));
        }

        $employees = array();
        foreach ($emps as $emp) {
            $empNew = new \stdClass();
            $empNew->id = $emp->employee;
            $empNew->payrollEmployeeId = $emp->id;
            $empNew->name = $employeeNamesById[$emp->employee];
            $employees[] = $empNew;
        }

        $column = new $columnTable();
        $columns = $column->Find(
            "enabled = ? and id in (".implode(",", $columnList).") order by colorder, id",
            array('Yes')
        );

        $cost = new $valueTable();
        $costs = $cost->Find("payroll = ?", array($req->payrollId));

        //Build value map
        $valueMap = array();
        foreach ($costs as $val) {
            if (!isset($valueMap[$val->employee])) {
                $valueMap[$val->employee] = array();
            }

            $valueMap[$val->employee][$val->payroll_item] = $val;
        }

        //Fill hours worked
        foreach ($employees as $e) {
            if ($payroll->status != "Completed") {
                foreach ($columns as $column) {
                    if (isset($valueMap[$e->id][$column->id]) && $column->editable == "Yes") {
                        $this->addToCalculationCache(
                            $column->id."-".$payroll->id."-".$e->id,
                            $valueMap[$e->id][$column->id]->amount
                        );
                        continue;
                    }
                    $item = new PayrollData();
                    $item->payroll = $req->payrollId;
                    $item->employee = $e->id;
                    $item->payroll_item = $column->id;
                    $item->amount = $this->calculatePayrollColumn($column, $payroll, $e->id, $e->payrollEmployeeId);
                    if ($item->amount == "") {
                        $item->amount =  $column->default_value;
                    }
                    $valueMap[$e->id][$column->id] = $item;
                }
            }
        }

        $values = array();
        foreach ($valueMap as $key => $val) {
            $values = array_merge($values, array_values($val));
        }

        if ($save == "1") {
            foreach ($values as $value) {
                if (empty($value->id)) {
                    $value->Save();
                }
            }
        }

        if ($payroll->status == 'Processing') {
            $payroll->status =  'Completed';
            $payroll->Save();
        }

        if ($payroll->status == 'Completed') {
            $newCols = array();
            foreach ($columns as $col) {
                $col->editable = 'No';
                $newCols[] = $col;
            }
            $columns = $newCols;
        }
        return new IceResponse(IceResponse::SUCCESS, array($employees,$columns,$values));
    }

    public function updateAllData($req)
    {

        $resp = $this->updateData($req);

        if ($resp->getStatus() == IceResponse::SUCCESS) {
            $payroll = new Payroll();
            $payroll->Load("id = ?", array($req->payrollId));
            $payroll->status = 'Processing';
            $ok = $payroll->Save();
            if (!$ok) {
                return new IceResponse(IceResponse::ERROR, $payroll->ErrorMsg());
            }
        }
        return $resp;
    }

    public function updateData($req)
    {
        $payroll = new Payroll();
        $payroll->Load("id = ?", array($req->payrollId));
        if ($payroll->status == 'Completed') {
            return new IceResponse(IceResponse::ERROR, true);
        }
        $valueTable = BaseService::getInstance()->getFullQualifiedModelClassName($req->valueTable);
        $payrollId = $req->payrollId;
        foreach ($req as $key => $val) {
            if (!is_array($val)) {
                continue;
            }
            $data = new $valueTable();
            $data->Load("payroll = ? and employee = ? and payroll_item = ?", array($payrollId,$val[1],$val[0]));
            if (empty($data->id)) {
                $data->payroll = $payrollId;
                $data->employee = $val[1];
                $data->payroll_item = $val[0];
            }
            $data->amount = $val[2];
            LogManager::getInstance()->info("Saving payroll data :".json_encode($data));
            $ok = $data->Save();
            if (!$ok) {
                LogManager::getInstance()->error("Error saving payroll data:".$data->ErrorMsg());
            }
        }

        return new IceResponse(IceResponse::SUCCESS, true);
    }

    public function deletePayrollGroup($req)
    {
        $employee = new PayrollEmployee();
        $report = new Payroll();
        $payrollGroup = new DeductionGroup();
        $payrollColumn = new PayrollColumn();
        $calcMethod =new Deduction();


        $payrollGroup->Load("id = ?", array($req->id));
        if (empty($payrollGroup->id)) {
            return new IceResponse(IceResponse::ERROR);
        }

        $this->baseService->checkSecureAccess('delete', $payrollGroup, 'DeductionGroup', $_POST);
        $this->baseService->checkSecureAccess('delete', $payrollColumn, 'payrollColumn', $_POST);
        $this->baseService->checkSecureAccess('delete', $calcMethod, 'Deduction', $_POST);

        $employee->Load("deduction_group = ?", $payrollGroup->id);
        $report->Load("deduction_group = ?", $payrollGroup->id);
        $payrollColumn->Load("deduction_group = ?", $payrollGroup->id);
        $calcMethod->Load("deduction_group = ?", $payrollGroup->id);

        if (!empty($employee->id)) {
            return new IceResponse(
                IceResponse::ERROR,
                "There are employees attached to this payroll group,
                 please re-assign employees to a different payroll group"
            );
        }

        if (!empty($report->id)) {
            return new IceResponse(
                IceResponse::ERROR,
                "There are payroll reports attached to this group, 
                please move the payroll reports to a different payroll group before deleting this group"
            );
        }

        BaseService::getInstance()->getDB()->Execute(
            'DELETE FROM PayrollColumns WHERE deduction_group= ? ',
            array($req->id)
        );
        BaseService::getInstance()->getDB()->Execute(
            'DELETE FROM Deductions WHERE deduction_group= ? ',
            array($req->id)
        );
        $ok = $payrollGroup->Delete();
        if (!$ok) {
            return new IceResponse(
                IceResponse::ERROR,
                "Error occurred while deleting payroll group"
            );
        }

        return new IceResponse(IceResponse::SUCCESS);
    }
}
