<?php
/**
 * This file is part of FacturaScripts
 * Copyright (C) 2021-2026 Carlos Garcia Gomez <carlos@facturascripts.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

namespace FacturaScripts\Core\Lib\AjaxForms;

use FacturaScripts\Core\DataSrc\Paises;
use FacturaScripts\Core\DataSrc\Series;
use FacturaScripts\Core\Lib\Calculator;
use FacturaScripts\Core\Lib\ExtendedController\BaseView;
use FacturaScripts\Core\Lib\ExtendedController\DocFilesTrait;
use FacturaScripts\Core\Lib\ExtendedController\LogAuditTrait;
use FacturaScripts\Core\Lib\ExtendedController\PanelController;
use FacturaScripts\Core\Model\Base\SalesDocument;
use FacturaScripts\Core\Model\Base\SalesDocumentLine;
use FacturaScripts\Core\Tools;
use FacturaScripts\Core\Where;
use FacturaScripts\Dinamic\Lib\AssetManager;
use FacturaScripts\Dinamic\Model\Cliente;
use FacturaScripts\Dinamic\Model\RoleAccess;
use FacturaScripts\Dinamic\Model\Variante;
use Throwable;

/**
 * Description of SalesController
 *
 * @author Carlos Garcia Gomez <carlos@facturascripts.com>
 */
abstract class SalesController extends PanelController
{
    use DocFilesTrait;
    use LogAuditTrait;

    const MAIN_VIEW_NAME = 'main';
    const MAIN_VIEW_TEMPLATE = 'Tab/SalesDocument';

    /** @var array */
    private $logLevels = ['critical', 'error', 'info', 'notice', 'warning'];

    abstract public function getModelClassName();

    public function getModel(bool $reload = false): SalesDocument
    {
        if ($reload) {
            $this->views[static::MAIN_VIEW_NAME]->model->clear();
        }

        // loaded record? just return it
        if ($this->views[static::MAIN_VIEW_NAME]->model->id()) {
            return $this->views[static::MAIN_VIEW_NAME]->model;
        }

        // get the record identifier
        $code = $this->request->queryOrInput('code');
        if (empty($code)) {
            // empty identifier? Then sets initial parameters to the new record and return it
            $formData = $this->request->query->all();
            SalesHeaderHTML::apply($this->views[static::MAIN_VIEW_NAME]->model, $formData);
            SalesFooterHTML::apply($this->views[static::MAIN_VIEW_NAME]->model, $formData);
            return $this->views[static::MAIN_VIEW_NAME]->model;
        }

        // existing record
        $this->views[static::MAIN_VIEW_NAME]->model->load($code);
        return $this->views[static::MAIN_VIEW_NAME]->model;
    }

    /**
     * @param SalesDocument $model
     * @param SalesDocumentLine[] $lines
     *
     * @return string
     */
    public function renderSalesForm(SalesDocument $model, array $lines): string
    {
        $url = empty($model->id()) ? $this->url() : $model->url();

        return '<div id="salesFormHeader">' . SalesHeaderHTML::render($model) . '</div>'
            . '<div id="salesFormLines">' . SalesLineHTML::render($lines, $model) . '</div>'
            . '<div id="salesFormFooter">' . SalesFooterHTML::render($model) . '</div>'
            . SalesModalHTML::render($model, $url);
    }

    public function series(string $type = ''): array
    {
        if (empty($type)) {
            return Series::all();
        }

        $list = [];
        foreach (Series::all() as $serie) {
            if ($serie->tipo == $type) {
                $list[] = $serie;
            }
        }

        return $list;
    }

    protected function autocompleteProductAction(): bool
    {
        $this->setTemplate(false);

        $list = [];
        $variante = new Variante();
        $query = (string)$this->request->queryOrInput('term');
        $where = [
            Where::eq('p.bloqueado', 0),
            Where::eq('p.sevende', 1)
        ];
        foreach ($variante->codeModelSearch($query, 'referencia', $where) as $value) {
            $list[] = [
                'key' => Tools::fixHtml($value->code),
                'value' => Tools::fixHtml($value->description)
            ];
        }

        if (empty($list)) {
            $list[] = ['key' => null, 'value' => Tools::trans('no-data')];
        }

        $this->response->json($list);
        return false;
    }

    protected function createViews()
    {
        $this->setTabsPosition('top');
        $this->createViewsDoc();
        $this->createViewDocFiles();
        $this->createViewLogAudit();
    }

    protected function createViewsDoc(): void
    {
        $pageData = $this->getPageData();
        $this->addHtmlView(static::MAIN_VIEW_NAME, static::MAIN_VIEW_TEMPLATE, $this->getModelClassName(), $pageData['title'], 'fa-solid fa-file');

        $route = Tools::config('route');
        AssetManager::addCss($route . '/node_modules/jquery-ui-dist/jquery-ui.min.css', 2);
        AssetManager::addJs($route . '/node_modules/jquery-ui-dist/jquery-ui.min.js', 2);

        SalesHeaderHTML::assets();
        SalesLineHTML::assets();
        SalesFooterHTML::assets();
        SalesModalHTML::assets();
    }

    protected function deleteDocAction(): bool
    {
        $this->setTemplate(false);

        // comprobamos los permisos
        if (false === $this->permissions->allowDelete) {
            Tools::log()->warning('not-allowed-delete');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $model = $this->getModel();
        if (false === $model->delete()) {
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $this->sendJsonWithLogs(['ok' => true, 'newurl' => $model->url('list')]);
        return false;
    }

    /**
     * @param string $action
     *
     * @return bool
     */
    protected function execPreviousAction($action)
    {
        // control de acceso: si se opera (o exporta) sobre un documento existente
        // que no pertenece al usuario, denegamos. Evita el acceso por código directo.
        $code = $this->request->queryOrInput('code');
        if (
            false === empty($action) && false === empty($code)
            && false === $this->checkOwnerData($this->getModel())
        ) {
            $this->setTemplate(false);
            Tools::log()->warning('access-denied');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        switch ($action) {
            case 'add-file':
                return $this->addFileAction();

            case 'autocomplete-product':
                return $this->autocompleteProductAction();

            case 'add-product':
            case 'fast-line':
            case 'fast-product':
            case 'new-line':
            case 'recalculate':
            case 'rm-line':
            case 'set-customer':
                return $this->recalculateAction(true);

            case 'delete-doc':
                return $this->deleteDocAction();

            case 'create-customer':
                return $this->createCustomerAction();

            case 'delete-file':
                return $this->deleteFileAction();

            case 'edit-file':
                return $this->editFileAction();

            case 'find-customer':
                return $this->findCustomerAction();

            case 'find-product':
                return $this->findProductAction();

            case 'recalculate-line':
                return $this->recalculateAction(false);

            case 'save-doc':
                $this->saveDocAction();
                return false;

            case 'save-paid':
                return $this->savePaidAction();

            case 'save-status':
                return $this->saveStatusAction();

            case 'sort-files':
                return $this->sortFilesAction();

            case 'unlink-file':
                return $this->unlinkFileAction();
        }

        return parent::execPreviousAction($action);
    }

    protected function createCustomerAction(): bool
    {
        $this->setTemplate(false);

        if (false === $this->canCreateCustomer()) {
            Tools::log()->warning('not-allowed-modify');
            $this->sendJsonWithLogs($this->emptySalesResponse());
            return false;
        }

        if (false === $this->validateFormToken()) {
            $this->sendJsonWithLogs($this->emptySalesResponse());
            return false;
        }

        $formData = json_decode($this->request->input('data'), true);
        if (false === is_array($formData)) {
            Tools::log()->warning('invalid-request');
            $this->sendJsonWithLogs($this->emptySalesResponse());
            return false;
        }

        foreach (['newcustomer_nombre', 'newcustomer_direccion', 'newcustomer_ciudad', 'newcustomer_provincia'] as $field) {
            if (false === is_string($formData[$field] ?? null) || trim($formData[$field]) === '') {
                Tools::log()->warning('invalid-request');
                $this->sendJsonWithLogs($this->emptySalesResponse());
                return false;
            }

            $formData[$field] = trim($formData[$field]);
        }

        $countryCode = is_string($formData['newcustomer_codpais'] ?? null) ? $formData['newcustomer_codpais'] : '';
        if (empty($countryCode) || false === Paises::get($countryCode)->exists()) {
            Tools::log()->warning('invalid-request');
            $this->sendJsonWithLogs($this->emptySalesResponse());
            return false;
        }

        $customer = new Cliente();
        $customer->cifnif = $formData['newcustomer_cifnif'] ?? '';
        $customer->codagente = $this->user->codagente;
        $customer->email = $formData['newcustomer_email'] ?? '';
        $customer->nombre = $formData['newcustomer_nombre'] ?? '';
        $customer->telefono1 = $formData['newcustomer_telefono'] ?? '';

        $this->db()->beginTransaction();
        try {
            if (false === $customer->save()) {
                $this->db()->rollback();
                $this->sendJsonWithLogs($this->emptySalesResponse());
                return false;
            }

            $address = $customer->getDefaultAddress();
            $address->cifnif = $customer->cifnif;
            $address->ciudad = $formData['newcustomer_ciudad'] ?? '';
            $address->codpais = $countryCode;
            $address->codpostal = $formData['newcustomer_codpostal'] ?? '';
            $address->direccion = $formData['newcustomer_direccion'] ?? '';
            $address->provincia = $formData['newcustomer_provincia'] ?? '';
            if (false === $address->save()) {
                $this->db()->rollback();
                $this->sendJsonWithLogs($this->emptySalesResponse());
                return false;
            }

            $this->db()->commit();
        } catch (Throwable $e) {
            $this->db()->rollback();
            Tools::log()->error($e->getMessage());
            $this->sendJsonWithLogs($this->emptySalesResponse());
            return false;
        }

        $formData['action'] = 'set-customer';
        $formData['codcliente'] = $customer->codcliente;

        $model = $this->getModel();
        $lines = $model->getLines();
        SalesHeaderHTML::apply($model, $formData);
        SalesFooterHTML::apply($model, $formData);
        SalesLineHTML::apply($model, $lines, $formData);
        Calculator::calculate($model, $lines, false);

        $this->sendJsonWithLogs([
            'customerCreated' => true,
            'footer' => SalesFooterHTML::render($model),
            'header' => SalesHeaderHTML::render($model),
            'lines' => SalesLineHTML::render($lines, $model),
            'linesMap' => [],
            'multireqtoken' => $this->multiRequestProtection->newToken(),
            'products' => '',
        ]);
        return false;
    }

    private function canCreateCustomer(): bool
    {
        if (false === $this->user->can('EditCliente', 'update')) {
            return false;
        }

        if ($this->user->admin || false === empty($this->user->codagente)) {
            return true;
        }

        foreach (RoleAccess::allFromUser($this->user->nick, 'EditCliente') as $access) {
            if (false === $access->onlyownerdata) {
                return true;
            }
        }

        return false;
    }

    protected function exportAction()
    {
        if (false === $this->permissions->allowExport) {
            Tools::log()->warning('no-print-permission');
            return;
        }

        $this->setTemplate(false);

        $subjectLang = $this->views[static::MAIN_VIEW_NAME]->model->getSubject()->langcode;
        $requestLang = $this->request->input('langcode');
        $langCode = $requestLang ?? $subjectLang ?? '';

        $this->exportManager->newDoc(
            $this->request->queryOrInput('option', ''),
            $this->title,
            (int)$this->request->input('idformat', ''),
            $langCode
        );
        $this->exportManager->addBusinessDocPage($this->views[static::MAIN_VIEW_NAME]->model);
        $this->exportManager->show($this->response);
    }

    protected function findCustomerAction(): bool
    {
        $this->setTemplate(false);

        // ¿El usuario tiene permiso para ver todos los clientes?
        $showAll = false;
        foreach (RoleAccess::allFromUser($this->user->nick, 'EditCliente') as $access) {
            if (false === $access->onlyownerdata) {
                $showAll = true;
            }
        }
        $where = [];
        if ($this->permissions->onlyOwnerData && !$showAll) {
            // Mantener alineado con OwnerDataTrait/getOwnerFilter si Cliente añade más criterios de propiedad.
            $where[] = Where::eq('codagente', $this->user->codagente);
            $where[] = Where::isNotNull('codagente');
        }

        $list = [];
        $customer = new Cliente();
        $term = $this->request->queryOrInput('term');
        foreach ($customer->codeModelSearch($term, '', $where) as $item) {
            $list[$item->code] = $item->code . ' | ' . Tools::fixHtml($item->description);
        }
        $this->response->json($list);
        return false;
    }

    protected function findProductAction(): bool
    {
        $this->setTemplate(false);
        $model = $this->getModel();
        $formData = json_decode($this->request->input('data'), true) ?? [];
        SalesHeaderHTML::apply($model, $formData);
        SalesFooterHTML::apply($model, $formData);
        SalesModalHTML::apply($model, $formData);
        $content = [
            'header' => '',
            'lines' => '',
            'linesMap' => [],
            'footer' => '',
            'products' => SalesModalHTML::renderProductList()
        ];
        $this->sendJsonWithLogs($content);
        return false;
    }

    /**
     * @param string $viewName
     * @param BaseView $view
     */
    protected function loadData($viewName, $view)
    {
        $code = $this->request->queryOrInput('code');

        switch ($viewName) {
            case 'docfiles':
                $this->loadDataDocFiles($view, $this->getModelClassName(), $code);
                break;

            case 'ListLogMessage':
                $this->loadDataLogAudit($view, $this->getModelClassName(), $code);
                break;

            case static::MAIN_VIEW_NAME:
                if (empty($code)) {
                    $this->getModel(true);
                    break;
                }

                $view->loadData($code);

                // ¿el usuario puede acceder a este documento?
                if (false === $this->checkOwnerData($view->model)) {
                    $this->setTemplate('Error/AccessDenied');
                    break;
                }

                // data not found?
                $action = $this->request->input('action', '');
                if ('' === $action && empty($view->model->primaryColumnValue())) {
                    Tools::log()->warning('record-not-found');
                    break;
                }

                $this->title .= ' ' . $view->model->primaryDescription();
                $view->settings['btnPrint'] = true;
                $view->addButton([
                    'action' => 'CopyModel?model=' . $this->getModelClassName() . '&code=' . $view->model->primaryColumnValue(),
                    'icon' => 'fa-solid fa-cut',
                    'label' => 'copy',
                    'type' => 'link'
                ]);
                break;
        }
    }

    protected function recalculateAction(bool $renderLines): bool
    {
        $this->setTemplate(false);
        $model = $this->getModel();
        $lines = $model->getLines();
        $formData = json_decode($this->request->input('data'), true) ?? [];
        SalesHeaderHTML::apply($model, $formData);
        SalesFooterHTML::apply($model, $formData);
        SalesLineHTML::apply($model, $lines, $formData);
        Calculator::calculate($model, $lines, false);

        $content = [
            'header' => SalesHeaderHTML::render($model),
            'lines' => $renderLines ? SalesLineHTML::render($lines, $model) : '',
            'linesMap' => $renderLines ? [] : SalesLineHTML::map($lines, $model),
            'footer' => SalesFooterHTML::render($model),
            'products' => '',
        ];
        $this->sendJsonWithLogs($content);
        return false;
    }

    protected function saveDocAction(bool $sendOk = true): bool
    {
        $this->setTemplate(false);

        // comprobamos los permisos
        if (false === $this->permissions->allowUpdate) {
            Tools::log()->warning('not-allowed-modify');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $model = $this->getModel();

        // si los datos del formulario no llegan o no son JSON válido (petición truncada,
        // límites post_max_size / max_input_vars), rechazamos en lugar de guardar en blanco
        $formData = json_decode($this->request->input('data'), true);
        if (false === is_array($formData)) {
            Tools::log()->warning('invalid-request');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        // bloqueo optimista: si el estado del documento ha cambiado desde que se cargó el formulario,
        // rechazamos para no borrar líneas a partir de un formulario obsoleto (tarea 4673)
        if (isset($formData['idestado']) && (int)$formData['idestado'] !== (int)$model->idestado) {
            Tools::log()->warning('document-state-changed');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $this->db()->beginTransaction();

        SalesHeaderHTML::apply($model, $formData);
        SalesFooterHTML::apply($model, $formData);

        if (false === $model->save()) {
            $this->db()->rollback();
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $lines = $model->getLines();
        SalesLineHTML::apply($model, $lines, $formData);
        Calculator::calculate($model, $lines, false);

        foreach ($lines as $line) {
            if (false === $line->save()) {
                $this->db()->rollback();
                $this->sendJsonWithLogs(['ok' => false]);
                return false;
            }
        }

        // remove missing lines
        foreach ($model->getLines() as $oldLine) {
            if (in_array($oldLine->idlinea, SalesLineHTML::getDeletedLines()) && false === $oldLine->delete()) {
                $this->db()->rollback();
                $this->sendJsonWithLogs(['ok' => false]);
                return false;
            }
        }

        $lines = $model->getLines();
        if (false === Calculator::calculate($model, $lines, true)) {
            $this->db()->rollback();
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $this->db()->commit();

        if ($sendOk) {
            $this->sendJsonWithLogs(['ok' => true, 'newurl' => $model->url() . '&action=save-ok']);
        }
        return true;
    }

    protected function savePaidAction(): bool
    {
        $this->setTemplate(false);

        // comprobamos los permisos
        if (false === $this->permissions->allowUpdate) {
            Tools::log()->warning('not-allowed-modify');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        // guardamos el documento
        if ($this->getModel()->editable && false === $this->saveDocAction()) {
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        // cargamos el modelo actualizado y los datos del form
        $model = $this->getModel();
        $formData = json_decode($this->request->input('data'), true);
        if (false === is_array($formData)) {
            Tools::log()->warning('invalid-request');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        // si la factura es de 0 €, la marcamos como pagada
        if (empty($model->total) && $model->hasColumn('pagada')) {
            $model->pagada = (bool)$formData['paid-status'];
            $model->save();
            $this->sendJsonWithLogs(['ok' => true, 'newurl' => $model->url() . '&action=save-ok']);
            return false;
        }

        // comprobamos si tiene recibos
        $receipts = $model->getReceipts();
        if (empty($receipts)) {
            Tools::log()->warning('invoice-has-no-receipts');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        // marcamos los recibos como pagados, eso marca la factura como pagada
        foreach ($receipts as $receipt) {
            $receipt->nick = $this->user->nick;
            // si no está pagado, actualizamos fechapago y codpago
            if (false == $receipt->pagado) {
                $receipt->fechapago = $formData['paid-date-modal'] ?? Tools::date();
                $receipt->codpago = $formData['paid-payment-modal'] ?? $model->codpago;
            }
            $receipt->pagado = (bool)$formData['paid-status'];
            if (false === $receipt->save()) {
                $this->sendJsonWithLogs(['ok' => false]);
                return false;
            }
        }

        $this->sendJsonWithLogs(['ok' => true, 'newurl' => $model->url() . '&action=save-ok']);
        return false;
    }

    protected function saveStatusAction(): bool
    {
        $this->setTemplate(false);

        // comprobamos los permisos
        if (false === $this->permissions->allowUpdate) {
            Tools::log()->warning('not-allowed-modify');
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        if ($this->getModel()->editable && false === $this->saveDocAction(false)) {
            return false;
        }

        $this->db()->beginTransaction();

        $model = $this->getModel();
        $model->idestado = (int)$this->request->input('selectedLine');

        try {
            if (false === $model->save()) {
                $this->db()->rollback();
                $this->sendJsonWithLogs(['ok' => false]);
                return false;
            }
        } catch (Throwable $e) {
            $this->db()->rollback();
            Tools::log()->error($e->getMessage());
            $this->sendJsonWithLogs(['ok' => false]);
            return false;
        }

        $this->db()->commit();
        $this->sendJsonWithLogs(['ok' => true, 'newurl' => $model->url() . '&action=save-ok']);
        return false;
    }

    private function emptySalesResponse(): array
    {
        return [
            'customerCreated' => false,
            'footer' => '',
            'header' => '',
            'lines' => '',
            'linesMap' => [],
            'multireqtoken' => $this->multiRequestProtection->newToken(),
            'products' => '',
        ];
    }

    private function sendJsonWithLogs(array $data): void
    {
        $data['messages'] = Tools::log()::read('master', $this->logLevels);
        $this->response->json($data);
    }
}
