<?php

/**
 * @package         Convert Forms
 * @version         5.1.1-RC10 Free
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace ConvertForms;

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\Registry\Registry;
use Joomla\Filesystem\Path;
use Joomla\Filesystem\File as JoomlaFile;
use ConvertForms\Helper;
use Joomla\CMS\Factory;
use NRFramework\File;

/**
 * Export submissions to CSV and JSON
 */
class Export
{
    /**
     * Export submissions to CSV or JSON file
     *
     * @param  array $options   The export options
     *
     * @return array
     */
    public static function export($options)
    {
        // Increase memory size and execution time to prevent PHP errors on datasets > 20K
        set_time_limit(300); // 5 Minutes
        ini_set('memory_limit', '-1');

        $options = new Registry($options);
        
        // Load submissions model
        BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_convertforms/models');
        $model = BaseDatabaseModel::getInstance('Conversions', 'ConvertFormsModel', ['ignore_request' => true]);

        // When we're exporting certain IDs, there's no need to check the state.
        if (strpos($options->get('filter_search', ''), 'id:') !== false || $options->get('filter_state') == '*')
        {
            $filter_state = 'skip';
        } else 
        {
            $filter_state = $options->get('filter_state');
        }

        $model->setState('filter.state', $filter_state);
        $model->setState('filter.join_campaigns', 'skip');
        $model->setState('filter.join_forms', 'skip');
        $model->setState('list.limit', $options->get('limit', 0));
        $model->setState('list.start', $options->get('offset', 0));
        $model->setState('list.direction', 'asc');
        $model->setState('filter.search', $options->get('filter_search'));
        $model->setState('filter.form_id', $options->get('filter_form_id'));
        $model->setState('filter.period', $options->get('filter_period', ''));
        $model->setState('filter.created_from', $options->get('filter_created_from', ''));
        $model->setState('filter.created_to', $options->get('filter_created_to', ''));
        $model->setState('filter.timezone', $options->get('filter_timezone', ''));

        // Proceed only if we have submissions
        if (!$submissions = $model->getItems())
        {
            $error = Text::sprintf('COM_CONVERTFORMS_NO_RESULTS_FOUND', strtolower(Text::_('COM_CONVERTFORMS_SUBMISSIONS')));
            throw new \Exception($error);
        }

        foreach ($submissions as $key => &$submission)
        {
            self::prepareSubmission($submission);
        }

        $export_type = $options->get('export_type', 'csv');

        $pagination = $model->getPagination();
        $firstRun = $pagination->pagesCurrent == 1;
        $append = !$firstRun || $options->get('export_append', false);

        // Check whether the path does exist. If not, attemp to create it.
        $export_path = $options->get('export_path', File::getTempFolder());

		if (!File::createDirs($export_path, false))
		{
            throw new \Exception('Export path does not exist');
		}

        $filename = Path::clean($export_path . DIRECTORY_SEPARATOR) . $options->get('filename', 'convertforms_submissions.' . $export_type);

        switch ($export_type)
        {
            case 'json':
                self::toJSON($submissions, $filename, $append);
                break;

            default:
                $excel_security = (bool) Helper::getComponentParams()->get('excel_security', true);
                self::toCSV($submissions, $filename, $append, $excel_security);
                break;
        }

        return [
            'options'    => $options,
            'pagination' => $pagination,
            'filename'   => $filename
        ];
    }

    /**
     *  Get a key value array with submission's submitted data
     *
     *  @param   object  $submission  The submission object
     *
     *  @return  array
     */
    private static function prepareSubmission(&$submission)
    {
        $result = [
            'id' => $submission->id,
            'created' => $submission->created,
            'state' => $submission->state
        ];

        foreach ($submission->prepared_fields as $field_name => $field)
        {
            // Always return the raw value and let the export type decide how the value should be displayed.
            $result[$field_name] = $field->value_raw;
        }

        $submission = $result;
    }

    /**
     * Create a JSON file with given data
     *
     * @param   array     $data           The data to populate the file   
     * @param   string    $destination    The path where the store the JSON file
     * @param   bool      $append         If true, given data will be appended to the end of the file.
     *
     * @return  void
     */
    private static function toJSON($data, $destination, $append = false, $check_for_duplicates = true)
    {
        $content = file_exists($destination) ? (array) json_decode(file_get_contents($destination), true) : [];
        $content = $append ? array_merge($content, $data) : $data;

        if ($content && $append && $check_for_duplicates)
        {   
            $newArr = [];

            foreach ($content as $row)
            {
                $newArr[$row['id']] = $row;
            }

            $content = $newArr;
        }

        $content = json_encode(array_values($content), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        // Save the file
        JoomlaFile::write($destination, $content);
    }

    /**
     *  Create a CSV file with given data
     *
     *  @param   array     $data            The data to populate the file   
     *  @param   string    $destination     The path where the store the CSV file
     *  @param   bool      $append          If true, given data will be appended to the end of the file.
     *  @param   boolean   $excel_security  If enabled, certain row values will be prefixed by a tab to avoid any CSV injection.
     *
     *  @return  void
     */
    private static function toCSV($data, $destination, $append = false, $excel_security = true, $check_for_duplicates = true)
    {
        $resource = fopen($destination, $append ? 'a+' : 'w');

        if (!$append)
        {
            // Support UTF-8 on Microsoft Excel
            fputs($resource, "\xEF\xBB\xBF");
            
            // Add column names in the first line
            fputcsv($resource, array_keys($data[0]));
        }

        // Get CSV content
        $existingRows = [];
        if ($append && $check_for_duplicates)
        {
            while (($existingData = fgetcsv($resource)) !== false)
            {
                $existingRows[(int) $existingData[0]] = $existingData;
            }
        }

        foreach ($data as $row)
        {
            if (!empty($existingRows) && isset($row['id']) && array_key_exists($row['id'], $existingRows))
            {
                continue;
            }

            // Prevent CSV Injection: https://vel.joomla.org/articles/2140-introducing-csv-injection
            if ($excel_security)
            {
                foreach ($row as &$value)
                {
                    $value = is_array($value) ? implode(', ', $value) : $value;

                    $firstChar = substr($value, 0, 1);

                    // Prefixe values starting with a =, +, - or @ by a tab character
                    if (in_array($firstChar, array('=', '+', '-', '@')))
                    {
                        $value = '    ' . $value;
                    }
                }
            }

            fputcsv($resource, $row);
        }

        fclose($resource);
    }

    /**
     * Redirects to the error layout and displays the given error message
     *
     * @param  string $error_message
     *
     * @return void
     */
    public static function error($error_message)
    {
        $app = Factory::getApplication();

        $optionsQuery = http_build_query(array_filter([
            'option' => 'com_convertforms',
            'view'   => 'export',
            'layout' => 'error',
            'error'  => $error_message,
            'tmpl'   => $app->input->get('tmpl')
        ]));
    
        $app->redirect('index.php?' . $optionsQuery);
    }

    /**
     * Verifies the export file does exist
     *
     * @param string $filename
     *
     * @return bool 
     */
    public static function exportFileExists($filename)
    {
        return file_exists(File::getTempFolder() . $filename);
    }

    /**
     * Adds the Export popup to the page which can be triggered by toolbar buttons.
     *
     * @return void
     */
    public static function renderModal()
    {
        HTMLHelper::script('com_convertforms/export.js', ['relative' => true, 'version' => 'auto']);

        Factory::getDocument()->addScriptDeclaration('
            document.addEventListener("DOMContentLoaded", function() {
                new exportModal("' . Factory::getApplication()->input->get('view') . '");
            });
        ');

        $html = HTMLHelper::_('bootstrap.renderModal', 'cfExportSubmissions', [
            'backdrop' => 'static'
        ]);

        // This is the only way to add a custom CSS class to the popup container
        echo str_replace('modal hide', 'modal hide transparent', $html);
    }
}