<?php

namespace Teknisa\Libs\Service;

use Zeedhi\Framework\DataSource\FilterCriteria;
use Zeedhi\Framework\DataSource\DataSet;
use Zeedhi\Framework\DTO\Response\Message;
use Zeedhi\Framework\DTO\Response\Notification;
use Zeedhi\Framework\DTO;
use Teknisa\Libs\Util\Environment;
use Zeedhi\Framework\DataSource\Manager\Doctrine\NameProvider;
use Teknisa\Libs\Util\NewCode;
use Zeedhi\Framework\DataSource\ParameterBag;
use Zeedhi\Framework\DataSource\Manager\SQL\ManagerImpl;

class CRUD {
    /** @var ManagerImpl  */
    protected $dataSourceManager;
    /** @var Environment  */
    protected $environment;
    /** @var NewCode */
    protected $newCode;
    /** @var NameProvider */
    protected $nameProvider;
    /** @var ParameterBag  */
    protected $parameterBag;

    private $columnsToReturn = array();

    public function __construct(ManagerImpl $dataSourceManager, Environment $environment, NewCode $newCode,
                                NameProvider $nameProvider, ParameterBag $parameterBag) {
        $this->dataSourceManager = $dataSourceManager;
        $this->environment = $environment;
        $this->newCode = $newCode;
        $this->nameProvider = $nameProvider;
        $this->parameterBag = $parameterBag;
    }

    public function getRowsToSave($request, $dataSourceName) {
        $rows = array();

        $dataSource = $this->nameProvider->getDataSourceByName($dataSourceName);
        $request = $this->buildDataSetIfRow($request, $dataSourceName);
        $currentTime = $this->environment->getCurrentTime()->format("d/m/Y H:i:s");

        $this->columnsToReturn = array();

        foreach ($request->getDataSet()->getRows() as $row) {
            if (is_object($row))
                $row = (array) $row;
            
            $row['NRORG'] = (isset($row['NRORG']) && !empty($row['NRORG'])) ? $row['NRORG'] : $this->environment->getNrOrgTrab();
            $row['IDATIVO'] = isset($row['IDATIVO']) ? ($row['IDATIVO'] ?: 'S') : 'S';

            /**
             * @todo Refatorar esta função
             */
            if ($row['__is_new']) {
                $row['DTULTATU'] = $currentTime;
                $row['NRORGULTATU'] = $this->environment->getNrOrgTrab();
                $row['CDOPERULTATU'] = $this->environment->getCdOperador();
                $row['DTINCLUSAO'] = $currentTime;
                $row['NRORGINCLUSAO'] = $this->environment->getNrOrgTrab();
                $row['CDOPERINCLUSAO'] = $this->environment->getCdOperador();

                if(!empty($row['__new_code_pk'])) {
                    $nrorg = $this->environment->getNrOrgTrab();

                    if (isset($row['__nrorgpadrao']))
                        $nrorg = $row['__nrorgpadrao'];

                    $castNumber = empty($row['__cast_number']) ? false : $row['__cast_number'];
                    $newCodeKey = empty($row['__using_unit']) ? $row['__new_code_pk'] : ($row['__new_code_pk'].$this->environment->getCdFilial());
                    $newCodeKey = strtoupper($newCodeKey);
                    $colSize = isset($row['__new_code_column_size']) ? $row['__new_code_column_size'] : 10;
                    $lenght = isset($row['__new_code_length']) ? $row['__new_code_length'] : 1;

                    $row[strtoupper($row['__table_pk'])] = $this->newCode->getCodeByTableName($newCodeKey, $nrorg, $lenght, $colSize, $castNumber);
                }

                $row = $this->getRowWithDefaultColumnsProperties($row, $dataSource);
                $this->validatePrimaryKeys($row, $dataSourceName);
            } else {
                $row['DTULTATU'] = $currentTime;
                $row['NRORGULTATU'] = $this->environment->getNrOrgTrab();
                $row['CDOPERULTATU'] = $this->environment->getCdOperador();
                $row = $this->getOthersFields($dataSourceName, $dataSource->getPrimaryKeyColumns(), $row);
            }

            if (isset($row['__fieldsToValidateDuplication'])) {
                $this->validateDuplicatedFields($row, $dataSourceName);
            }

            $rows[] = $row;

            $this->buildColumnsToReturn($row);
        }

        return $rows;
    }

    public function getConditionsByColumns(array $columns, array $row, $isNew = false) {
        $conditions = array();

        foreach ($columns as $column) {
            if ($isNew && !isset($row[$column])) {
                throw new \Exception("Column $column cannot be null.", 1);
            }

            $conditions[] = array(
                'columnName' => $column,
                'operator' => '=',
                'value' => $row[$column],
            );
        }

        return $conditions;
    }

    public function getOthersFields($dataSourceName, $primaryKeys, $row) {
        $conditions = $this->getConditionsByColumns($primaryKeys, $row);
        $dataSet = $this->getFilteredDataSet($dataSourceName, $conditions);
        
        $dataSetRows = $dataSet->getRows();

        if (count($dataSetRows) !== 1)
            throw new \Exception('Cannot find row to update.', 1);

        return array_merge($dataSetRows[0], $row);
    }

    public function buildColumnsToReturn($row) {
        if (\Zeedhi\Framework\Util\Functions::arrayKeyExists('__columns_to_return', $row)) {
            foreach ($row['__columns_to_return'] as $column) {
                $columnToReturn[$column] = $row[$column];
            }

            $this->addColumnToReturn($columnToReturn);
        }
    }

    public function addColumnToReturn(array $value) {
        $this->columnsToReturn[] = $value;
    }

    public function getColumnsToReturn() {
        return $this->columnsToReturn;
    }

    public function getRowsToDelete($request, $dataSourceName){
        $rows = array();

        $dataSource = $this->nameProvider->getDataSourceByName($dataSourceName);
        $request = $this->buildDataSetIfRow($request, $dataSourceName);
        $primaryKeys = $dataSource->getPrimaryKeyColumns();

        foreach ($request->getDataSet()->getRows() as $row) {
            $rowToDelete = array();

            if(!isset($row['NRORG']))
                $row['NRORG'] = $this->environment->getNrOrgTrab();

            foreach ($primaryKeys as $primaryKey)
                $rowToDelete[$primaryKey] = $row[$primaryKey];

            $rowToDelete['__is_new'] = false;
            $rows[] = $rowToDelete;
        }

        return $rows;
    }

    public function save($dataSourceName, $rows) {
        $dataSet = new DataSet($dataSourceName, $rows);
        $this->dataSourceManager->persist($dataSet);
    }

    public function find($response, $filterCriteria) {
        $dataSet = $this->dataSourceManager->findBy($filterCriteria);
        $response->addDataSet($dataSet);
    }

    public function delete($response, $dataSet) {
        $this->dataSourceManager->delete($dataSet);
        $response->addNotification(new Notification("Excluído com sucesso."));
    }

    public function buildDataSetIfRow(DTO\Request $request, $dataSourceName) {
        if ($request instanceof DTO\Request\Row) {
            $dataSet = new DataSet($dataSourceName, array((array)$request->getRow()));
            $request = new DTO\Request\DataSet($dataSet, $request->getMethod(), $request->getRoutePath(), $request->getUserId());
        }

        return $request;
    }

    public function validatePrimaryKeys($row, $dataSourceName) {
        $primaryKeys = $this->nameProvider->getDataSourceByName($dataSourceName)->getPrimaryKeyColumns();
        $conditions = $this->getConditionsByColumns($primaryKeys, $row, true);

        $dataSet = $this->getFilteredDataSet($dataSourceName, $conditions);
        $test = $dataSet->getRows();

        if (!empty($test)){
            throw new \Exception("Impossível adicionar. Registro já existe.", 1);
        }
    }

    private function getRowWithDefaultColumnsProperties($row, $dataSource) {
        if (method_exists($dataSource, 'getColumnsProperties')) {
            $columnsProperties = $dataSource->getColumnsProperties();

            if (isset($columnsProperties)) {
                foreach ($columnsProperties as $columnProperty) {
                    if (!\Zeedhi\Framework\Util\Functions::arrayKeyExists($columnProperty['COLUMN_NAME'], $row)) {
                        if ($columnProperty['NULLABLE'] == 'Y') {
                            $row[$columnProperty['COLUMN_NAME']] = $this->getDataDefaultValue($columnProperty);
                        } else if ($columnProperty['COLUMN_NAME'] == 'CDFILIAL') {
                            $row[$columnProperty['COLUMN_NAME']] = $this->environment->getCdFilial();
                        } else if ($columnProperty['NULLABLE'] == 'N') {
                            if (isset($columnProperty['DATA_DEFAULT'])){
                                $row[$columnProperty['COLUMN_NAME']] = $this->getDataDefaultValue($columnProperty);
                            } else {
                                throw new \Exception("Column ".$columnProperty['COLUMN_NAME']." cannot be null and does not have a default value! Set a value for this column.", 1);
                            }
                        }
                    }
                }
            }
        }

        return $row;
    }

    private function getDataDefaultValue($columnProperty) {
        if ($columnProperty['DATA_DEFAULT'] == null) {
            return null;
        } else {
            switch ($columnProperty['DATA_TYPE']) {
                case 'DATE':
                    return $this->environment->getCurrentTime()->format("d/m/Y H:i:s");
                    break;
                case 'NUMBER':
                    return intval($columnProperty['DATA_DEFAULT']);
                    break;
                case 'FLOAT':
                    return floatval($columnProperty['DATA_DEFAULT']);
                    break;
                default:
                    return $columnProperty['DATA_DEFAULT'];
                    break;
            }
        }
    }

    public function getDataSourceNameToSaveByName($name){
        $dataSource = $this->nameProvider->getDataSourceByName($name);

        return strtolower($dataSource->getTableName());
    }

    /**
     * @param FilterCriteria $filterCriteria
     * @return FilterCriteria
     */
    public function getFilterCriteriaWithNrOrg($filterCriteria) {
        $conditions = $filterCriteria->getConditions();

        foreach ($conditions as &$condition) {
            if ($condition['value'] === 'FILIAL_LOGADA') {
                $condition['value'] = $this->environment->getCdFilial();
            } else if ($condition['value'] === 'OPERADOR_LOGADO') {
                $condition['value'] = $this->environment->getCdOperador();
            }
        }

        $newFilterCriteria = new FilterCriteria($filterCriteria->getDataSourceName(), $conditions,
            $filterCriteria->getPage(), $filterCriteria->getPageSize());

        $this->keepOrderBy($filterCriteria, $newFilterCriteria);

        $nrorgFilter = array_filter($conditions, function($item) {
            return strtoupper($item['columnName']) == 'NRORG';
        });

        if (empty($nrorgFilter)) {
            $newFilterCriteria->addCondition('NRORG', '=', $this->environment->getNrOrgTrab());
        }

        return $newFilterCriteria;
    }

    protected function processConditionValue($condition) {
        return $condition['value'];
    }

    public function populateParameterBag(FilterCriteria $filterCriteria, $parameterBagColumns) {
        $conditions = array();

        foreach ($filterCriteria->getConditions() as $condition) {
            $columnName = $condition['columnName'];
            if (in_array($columnName, $parameterBagColumns)) {
                $this->parameterBag->set($columnName, $this->processConditionValue($condition));
            } else {
                $conditions[] = $condition;
            }
        }

        $newFilterCriteria = new FilterCriteria(
            $filterCriteria->getDataSourceName(),
            $conditions,
            $filterCriteria->getPage(),
            $filterCriteria->getPageSize()
        );

        $this->keepOrderBy($filterCriteria, $newFilterCriteria);

        return $newFilterCriteria;
    }

    public function getFilteredDataSet($dataSourceName, $conditions, $page = null, $pageSize = null) {
        $filterCriteria = new FilterCriteria($dataSourceName, $conditions, $page, $pageSize);
        $filterCriteria = $this->getFilterCriteriaWithNrOrg($filterCriteria);
        $filterCriteria = $this->getPopulatedFilterCriteria($filterCriteria, array());
        $dataSet = $this->dataSourceManager->findBy($filterCriteria);

        return $dataSet;
    }

    public function getEnvironment() {
        return $this->environment;
    }

    /**
     * Keep old filter order by to new filter
     * @param  FilterCriteria $oldFilterCriteria
     * @param  FilterCriteria $newFilterCriteria
     */
    public function keepOrderBy(FilterCriteria $oldFilterCriteria, FilterCriteria $newFilterCriteria) {
        $orderBy = $oldFilterCriteria->getOrderBy();

        if (!empty($orderBy)) {
            foreach ($orderBy as $key => $value) {
                $newFilterCriteria->addOrderBy($key, $value);
            }
        }
    }

    /**
     * @param FilterCriteria $filterCriteria
     * @param $parameterBagColumns
     * @return FilterCriteria
     */
    public function getPopulatedFilterCriteria($filterCriteria, $parameterBagColumns) {
        $dataSourceName = $filterCriteria->getDataSourceName();
        $query = $this->nameProvider->getDataSourceByName($dataSourceName)->getQuery();

        if ($query) {
            if (empty($parameterBagColumns)) {
                $parameterBagColumns = $this->buildParamBag($filterCriteria, $query);
            }

            $filterCriteria = $this->populateParameterBag($filterCriteria, $parameterBagColumns);
        }

        return $filterCriteria;
    }

    /**
     * @param FilterCriteria $filterCriteria
     * @param $query
     * @return array
     */
    private function buildParamBag($filterCriteria, $query) {
        $parameterBagColumns = array();
        $query = strtoupper($query);

        foreach ($filterCriteria->getConditions() as $condition) {
            $columnName = strtoupper($condition['columnName']);

            if (strpos($query, ':'.$columnName)) {
                $parameterBagColumns[] = $columnName;
            }
        }

        return $parameterBagColumns;
    }

    private function validateDuplicatedFields($row, $dataSourceName){
        $fieldsToValidate = $row['__fieldsToValidateDuplication'];

        foreach($fieldsToValidate as $field) {
            $fieldName = $field['name'];
            $conditions = [
                array(
                    "columnName" => $fieldName,
                    "operator"   => '=',
                    "value"      => $row[$fieldName]
                ),
                array(
                    "columnName" => "NRORG",
                    "operator"   => '=',
                    "value"      => $this->environment->getNrOrgTrab()
                )
            ];

            $filterCriteria = new FilterCriteria($dataSourceName, $conditions);

            $newFilterCriteria = $this->getPopulatedFilterCriteria($filterCriteria, array());

            $dataSet = $this->dataSourceManager->findBy($newFilterCriteria);

            if(current($dataSet->getRows())) {
                $errorMsg = "O valor informado para o campo de ".$field['description']." já foi cadastrado.";
                throw new \Exception($errorMsg, 1);
            }
        }
    }
}