<?php

/**
 * @package         Tassos Code Snippets
 * @version         1.0.1-RC2 Free
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2026 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace Tassos\CodeSnippets\Administrator\Model;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Filter\OutputFilter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

/**
 * Snippets list model class.
 */
class SnippetsModel extends ListModel
{
    /**
     * The file extension for snippet export/import files.
     *
     * @var string
     */
    private $fileExtension = '.tcs';

    /**
     * Constructor.
     *
     * @param   array  $config  An optional associative array of configuration settings.
     */
    public function __construct($config = [])
    {
        if (empty($config['filter_fields']))
        {
            $config['filter_fields'] = [
                'id', 'a.id',
                'title', 'a.title',
                'state', 'a.state',
                'location', 'a.location',
                'ordering', 'a.ordering',
                'catid', 'a.catid', 'category_title',
                'access', 'a.access', 'access_level',
                'language', 'a.language',
                'created', 'a.created',
                'created_by', 'a.created_by',
                'modified', 'a.modified',
                'modified_by', 'a.modified_by',
                'tag'
            ];
        }

        parent::__construct($config);
    }

    /**
     * Method to auto-populate the model state.
     *
     * @param   string  $ordering   An optional ordering field.
     * @param   string  $direction  An optional direction (asc|desc).
     *
     * @return  void
     */
    protected function populateState($ordering = 'a.ordering', $direction = 'asc')
    {
        $app = Factory::getApplication();

        // Adjust the context to support modal layouts.
        if ($layout = $app->input->get('layout'))
        {
            $this->context .= '.' . $layout;
        }

        $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search');
        $this->setState('filter.search', $search);

        $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '');
        $this->setState('filter.state', $state);

        $location = $this->getUserStateFromRequest($this->context . '.filter.location', 'filter_location', '');
        $this->setState('filter.location', $location);

        $categoryId = $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '');
        $this->setState('filter.category_id', $categoryId);

        $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '');
        $this->setState('filter.access', $access);

        $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '');
        $this->setState('filter.language', $language);

        $tag = $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', '');
        $this->setState('filter.tag', $tag);

        parent::populateState($ordering, $direction);
    }

    /**
     * Method to get a store id based on model configuration state.
     *
     * @param   string  $id  A prefix for the store id.
     *
     * @return  string  A store id.
     */
    protected function getStoreId($id = '')
    {
        $id .= ':' . $this->getState('filter.search');
        $id .= ':' . $this->getState('filter.state');
        $id .= ':' . $this->getState('filter.location');
        $id .= ':' . $this->getState('filter.category_id');
        $id .= ':' . serialize($this->getState('filter.access'));
        $id .= ':' . serialize($this->getState('filter.language'));
        $id .= ':' . serialize($this->getState('filter.tag'));

        return parent::getStoreId($id);
    }

    /**
     * Build an SQL query to load the list data.
     *
     * @return  \Joomla\Database\DatabaseQuery
     */
    protected function getListQuery()
    {
        $db    = $this->getDatabase();
        $query = $db->getQuery(true);
        $user  = Factory::getApplication()->getIdentity();

        // Select required fields
        $query->select(
            $this->getState(
                'list.select',
                [
                    $db->quoteName('a.id'),
                    $db->quoteName('a.title'),
                    $db->quoteName('a.alias'),
                    $db->quoteName('a.code'),
                    $db->quoteName('a.code_type'),
                    $db->quoteName('a.insertion_method'),
                    $db->quoteName('a.location'),
                    $db->quoteName('a.params'),
                    $db->quoteName('a.publishing_rules'),
                    $db->quoteName('a.state'),
                    $db->quoteName('a.ordering'),
                    $db->quoteName('a.catid'),
                    $db->quoteName('a.language'),
                    $db->quoteName('a.access'),
                    $db->quoteName('a.checked_out'),
                    $db->quoteName('a.checked_out_time'),
                    $db->quoteName('a.created'),
                    $db->quoteName('a.created_by'),
                    $db->quoteName('a.modified'),
                    $db->quoteName('a.modified_by'),
                    $db->quoteName('a.note'),
                    $db->quoteName('a.last_error'),
                    $db->quoteName('a.last_error_date'),
                ]
            )
        )
            ->from($db->quoteName('#__tassos_codesnippets', 'a'));

        // Join over the category
        $query->select($db->quoteName('c.title', 'category_title'))
            ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid'));

        // Join over the access level
        $query->select($db->quoteName('ag.title', 'access_level'))
            ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access'));

        // Join over the users for the author
        $query->select($db->quoteName('ua.name', 'author_name'))
            ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by'));

        // Join over the language
        $query->select($db->quoteName('l.title', 'language_title'))
            ->select($db->quoteName('l.image', 'language_image'))
            ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language'));

        // Filter by state
        $state = $this->getState('filter.state');

        if (is_numeric($state))
        {
            $state = (int) $state;
            $query->where($db->quoteName('a.state') . ' = :state')
                ->bind(':state', $state, ParameterType::INTEGER);
        }
        elseif ($state === '')
        {
            $query->where($db->quoteName('a.state') . ' IN (0, 1, -3)');
        }

        // Filter by location
        $location = $this->getState('filter.location');

        if ($location)
        {
            $query->where($db->quoteName('a.location') . ' = :location')
                ->bind(':location', $location);
        }

        // Filter by category
        $categoryId = $this->getState('filter.category_id');

        if (is_numeric($categoryId))
        {
            $categoryId = (int) $categoryId;
            $query->where($db->quoteName('a.catid') . ' = :catid')
                ->bind(':catid', $categoryId, ParameterType::INTEGER);
        }

        // Filter by access level
        $access = $this->getState('filter.access');

        if (is_array($access) && !empty($access))
        {
            $access = ArrayHelper::toInteger($access);
            $query->whereIn($db->quoteName('a.access'), $access);
        }
        elseif (is_numeric($access))
        {
            $access = (int) $access;
            $query->where($db->quoteName('a.access') . ' = :access')
                ->bind(':access', $access, ParameterType::INTEGER);
        }

        // Filter on the language
        $language = $this->getState('filter.language');

        if ($language)
        {
            if (is_array($language))
            {
                $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING);
            }
            else
            {
                $query->where($db->quoteName('a.language') . ' = :language')
                    ->bind(':language', $language);
            }
        }

        // Filter by tag
        $tag = $this->getState('filter.tag');

        if (!empty($tag))
        {
            $tag = ArrayHelper::toInteger((array) $tag);

            if ($tag)
            {
                $query->join(
                    'INNER',
                    $db->quoteName('#__contentitem_tag_map', 'tagmap'),
                    $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id')
                        . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_tassoscodesnippets.snippet')
                )
                    ->whereIn($db->quoteName('tagmap.tag_id'), $tag);
            }
        }

        // Filter by search in title
        $search = $this->getState('filter.search');

        if (!empty($search))
        {
            if (stripos($search, 'id:') === 0)
            {
                $search = (int) substr($search, 3);
                $query->where($db->quoteName('a.id') . ' = :search')
                    ->bind(':search', $search, ParameterType::INTEGER);
            }
            else
            {
                $search = '%' . str_replace(' ', '%', trim($search)) . '%';
                $query->where('(' . $db->quoteName('a.title') . ' LIKE :search OR ' . $db->quoteName('a.code') . ' LIKE :search2)')
                    ->bind(':search', $search)
                    ->bind(':search2', $search);
            }
        }

        // Add the list ordering clause
        $orderCol  = $this->state->get('list.ordering', 'a.ordering');
        $orderDirn = $this->state->get('list.direction', 'ASC');

        if ($orderCol === 'a.ordering' || $orderCol === 'category_title')
        {
            $ordering = [
                $db->quoteName('c.title') . ' ' . $db->escape($orderDirn),
                $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn),
            ];
        }
        else
        {
            $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn);
        }

        $query->order($ordering);

        return $query;
    }

    /**
     * Method to get an array of data items.
     *
     * @return  array  An array of data items on success, empty array on failure.
     */
    public function getItems()
    {
        $items = parent::getItems();

        if (empty($items))
        {
            return [];
        }

        // Expand params into item properties
        foreach ($items as &$item)
        {
            if (!empty($item->params))
            {
                $params = is_string($item->params) ? json_decode($item->params, true) : $item->params;

                if (is_array($params))
                {
                    foreach ($params as $key => $value)
                    {
                        if (!isset($item->$key) && !is_object($value))
                        {
                            $item->$key = $value;
                        }
                    }
                }
            }
        }

        return $items;
    }

    /**
     * Method to export selected snippets as a downloadable JSON file.
     *
     * @param   array  $ids  Array of snippet IDs to export.
     *
     * @return  void
     */
    public function export(array $ids)
    {
        $db  = $this->getDatabase();
        $ids = array_map('intval', $ids);

        $query = $this->getListQuery()
            ->clear('where')
            ->clear('order')
            ->whereIn($db->quoteName('a.id'), $ids);

        $db->setQuery($query);
        $rows = $db->loadAssocList();

        // Fields to strip from the export
        $strip = ['id', 'asset_id', 'checked_out', 'checked_out_time', 'created_by', 'modified_by',
                  'created', 'modified', 'ordering', 'author_name', 'category_title', 'access_level',
                  'language_title', 'language_image'];

        foreach ($rows as &$row)
        {
            $row['params']           = !empty($row['params']) ? json_decode($row['params'], true) : [];
            $row['publishing_rules'] = !empty($row['publishing_rules']) ? json_decode($row['publishing_rules'], true) : [];

            foreach ($strip as $key)
            {
                unset($row[$key]);
            }
        }

        $filename = count($rows) === 1 ? $rows[0]['title'] : 'snippets-' . date('Y-m-d');

        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $filename . $this->fileExtension . '"');
        echo json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        die;
    }

    /**
     * Method to import snippets from an uploaded .tcs file.
     *
     * @param   \Joomla\CMS\MVC\Model\AdminModel  $model  The snippet admin model used to save records.
     * @param   array                             $file   The uploaded file array from Joomla input.
     *
     * @return  void
     */
    public function import($model, array $file)
    {
        $app = Factory::getApplication();

        // Validate extension
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

        if ($ext !== ltrim($this->fileExtension, '.'))
        {
            $app->enqueueMessage(Text::_('COM_TASSOSCODESNIPPETS_IMPORT_ERROR_INVALID_FILE'), 'error');
            $app->redirect('index.php?option=com_tassoscodesnippets&view=snippets&layout=import');
            return;
        }

        $data  = file_get_contents($file['tmp_name']);
        $items = json_decode($data, true);

        if (!is_array($items) || empty($items))
        {
            $app->enqueueMessage(Text::_('COM_TASSOSCODESNIPPETS_IMPORT_ERROR_INVALID_FILE'), 'error');
            $app->redirect('index.php?option=com_tassoscodesnippets&view=snippets&layout=import');
            return;
        }

        $success = 0;
        $errors  = [];

        foreach ($items as $item)
        {
            // Build save data
            $saveData = [
                'id'               => 0,
                'title'            => $item['title'] ?? '',
                'alias'            => !empty($item['alias']) ? $item['alias'] : OutputFilter::stringURLSafe($item['title'] ?? 'snippet'),
                'code'             => $item['code'] ?? '',
                'code_type'        => $item['code_type'] ?? 'html',
                'insertion_method' => $item['insertion_method'] ?? 'auto',
                'location'         => $item['location'] ?? '',
                'params'           => json_encode($item['params'] ?? []),
                'publishing_rules' => $item['publishing_rules'] ?? [],
                'state'            => 0,
                'catid'            => (int) ($item['catid'] ?? 0),
                'access'           => (int) ($item['access'] ?? 1),
                'language'         => $item['language'] ?? '*',
                'note'             => $item['note'] ?? '',
            ];

            if ($model->save($saveData))
            {
                $success++;
            }
            else
            {
                $errors[] = $model->getError();
            }
        }

        if ($success > 0)
        {
            $app->enqueueMessage(Text::plural('COM_TASSOSCODESNIPPETS_N_SNIPPETS_IMPORTED', $success));
        }

        foreach ($errors as $error)
        {
            $app->enqueueMessage($error, 'error');
        }

        $app->redirect('index.php?option=com_tassoscodesnippets&view=snippets');
    }

}
