<?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\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Versioning\VersionableModelTrait;
use Joomla\CMS\Helper\TagsHelper;

/**
 * Snippet model.
 */
class SnippetModel extends AdminModel
{
    use VersionableModelTrait;

    /**
     * The prefix to use with controller messages.
     *
     * @var    string
     */
    protected $text_prefix = 'COM_TASSOSCODESNIPPETS_SNIPPET';

    /**
     * The type alias for content type
     *
     * @var    string
     */
    public $typeAlias = 'com_tassoscodesnippets.snippet';

    /**
     * Batch copy/move command.
     *
     * @var    string
     */
    protected $batch_copymove = 'category_id';

    /**
     * Allowed batch commands.
     *
     * @var    array
     */
    protected $batch_commands = [
        'assetgroup_id' => 'batchAccess',
        'language_id'   => 'batchLanguage',
        'tag'           => 'batchTag',
    ];

    /**
     * Method to get the record form.
     *
     * @param   array    $data      Data for the form.
     * @param   boolean  $loadData  True if the form is to load its own data.
     *
     * @return  Form|boolean  A Form object on success, false on failure.
     */
    public function getForm($data = [], $loadData = true)
    {
        $form = $this->loadForm('com_tassoscodesnippets.snippet', 'snippet', ['control' => 'jform', 'load_data' => $loadData]);

        if (empty($form))
        {
            return false;
        }

        return $form;
    }

    /**
     * Preprocess the form.
     *
     * @param   Form    $form   The form object
     * @param   mixed   $data   The data to be merged into the form object
     * @param   string  $group  The plugin group to be executed
     *
     * @return  void
     */
    protected function preprocessForm(Form $form, $data, $group = 'content')
    {
        // Add the rules form path from the framework
        $form->addFieldPath(JPATH_PLUGINS . '/system/nrframework/fields');

        // Convert to object for consistent access
        $data = (object) $data;

        // Get the record ID
        $id = $data->id ?? 0;

        // Get the current code_type value (default to html)
        $codeType = $data->code_type ?? 'html';

        // Load the type-specific form file
        $typeFormFile = JPATH_ADMINISTRATOR . '/components/com_tassoscodesnippets/forms/snippet_' . $codeType . '.xml';
        if (file_exists($typeFormFile))
        {
            $form->loadFile($typeFormFile, false);
        }

        // For existing snippets, always populate the shortcode display value.
        // Visibility is already controlled by showon="insertion_method:shortcode" in the XML,
        // so the value must be available regardless of the currently saved insertion_method —
        // otherwise switching to Shortcode on an existing 'auto' snippet shows the placeholder.
        if ($id > 0 && !empty($data->alias))
        {
            $form->setValue('shortcode_display', null, '{tcs snippet.' . $data->alias . '}');
        }

        // For new snippets, enable page reload when code_type changes
        if ($id == 0) {
            $form->setFieldAttribute('code_type', 'refresh-enabled', true);
            $form->setFieldAttribute('code_type', 'refresh-cat-id', $codeType);
            $form->setFieldAttribute('code_type', 'refresh-section', 'snippet');
        } else {
            // For existing snippets, disable the code_type field
            $form->setFieldAttribute('code_type', 'readonly', 'true');
        }

        parent::preprocessForm($form, $data, $group);
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return  mixed  The data for the form.
     */
    protected function loadFormData()
    {
        $app  = Factory::getApplication();
        $data = $app->getUserState('com_tassoscodesnippets.edit.snippet.data', []);

        if (empty($data))
        {
            $data = $this->getItem();

            // Pre-select some filters in edit form if none are set
            if ($this->getState('snippet.id') == 0)
            {
                $filters = (array) $app->getUserState('com_tassoscodesnippets.snippets.filter');

                $data->catid = $filters['category_id'] ?? $app->input->getInt('filter_category_id', $data->catid ?? 0);
                $data->language = $filters['language'] ?? $app->input->getString('filter_language', $data->language ?? '*');
                $data->access = $filters['access'] ?? $app->input->getInt('filter_access', $data->access ?? 1);
            }
        }

        $this->preprocessData('com_tassoscodesnippets.snippet', $data);

        return $data;
    }

    /**
     * Method to get a single record.
     *
     * @param   integer  $pk  The id of the primary key.
     *
     * @return  \Joomla\CMS\Object\CMSObject|boolean  Object on success, false on failure.
     */
    public function getItem($pk = null)
    {
        $item = parent::getItem($pk);

        if ($item)
        {
            // Load tags
            if (!empty($item->id))
            {
                $item->tags = new TagsHelper();
                $item->tags->getTagIds($item->id, 'com_tassoscodesnippets.snippet');
            }

            // Decode publishing_rules JSON
            if (!empty($item->publishing_rules) && is_string($item->publishing_rules))
            {
                $item->publishing_rules = json_decode($item->publishing_rules, true);
            }

            // Expand params into item properties
            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;
                        }
                    }
                }

                unset($item->params);
            }
        }

        return $item;
    }

    /**
     * Prepare and sanitise the table prior to saving.
     *
     * @param   Table  $table  A reference to a Table object.
     *
     * @return  void
     */
    protected function prepareTable($table)
    {
        // Reorder the snippets within the category for new items
        if (empty($table->id))
        {
            $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0');
        }
    }

    /**
     * Method to save the form data.
     *
     * @param   array  $data  The form data.
     *
     * @return  boolean  True on success.
     */
    public function save($data)
    {
        // Encode the publishing_rules as JSON
        if (isset($data['publishing_rules']) && is_array($data['publishing_rules']))
        {
            $data['publishing_rules'] = json_encode($data['publishing_rules']);
        }

        $requestedAlias = isset($data['alias']) ? (string) $data['alias'] : '';

        if (empty($data['id']))
        {
            // New record: derive alias from title if blank, then guarantee uniqueness.
            if (empty($data['alias']))
            {
                $data['alias'] = OutputFilter::stringURLSafe($data['title'] ?? '');
            }

            $table = $this->getTable();

            if ($table->load(['alias' => $data['alias'], 'catid' => $data['catid'] ?? 0]))
            {
                $aliasCollision = true;
            }

            [, $data['alias']] = $this->generateNewTitle(
                $data['catid'] ?? 0,
                $data['alias'],
                $data['title'] ?? ''
            );
        }
        else
        {
            // Existing record: check if the alias collides with any OTHER record in the same category.
            if (!empty($data['alias']))
            {
                $table = $this->getTable();

                if ($table->load(['alias' => $data['alias'], 'catid' => $data['catid'] ?? 0])
                    && (int) $table->id !== (int) $data['id'])
                {
                    $aliasCollision = true;

                    [, $data['alias']] = $this->generateNewTitle(
                        $data['catid'] ?? 0,
                        $data['alias'],
                        $data['title'] ?? ''
                    );
                }
            }
        }

        // Warn the user if the requested alias was changed because of a collision.
        if (!empty($aliasCollision) && $requestedAlias !== '' && $requestedAlias !== $data['alias'])
        {
            Factory::getApplication()->enqueueMessage(
                Text::sprintf('COM_TASSOSCODESNIPPETS_SHORTCODE_ALIAS_CHANGED_WARNING', $requestedAlias, $data['alias']),
                'warning'
            );
        }

        // Clear error data when manually (re-)publishing a snippet
        if (isset($data['state']) && (int) $data['state'] === 1)
        {
            $data['last_error']      = null;
            $data['last_error_date'] = null;
        }

        return parent::save($data);
    }

    /**
     * Method to validate form data.
     * 
     * Separates DB columns from extra fields, storing extras in params JSON.
     *
     * @param   Form    $form   The form to validate against.
     * @param   array   $data   The data to validate.
     * @param   string  $group  The name of the field group to validate.
     *
     * @return  array|boolean  Array of filtered data if valid, false otherwise.
     */
    public function validate($form, $data, $group = null)
    {
        // Separate DB columns from params BEFORE parent validation
        // because parent::validate() filters out unknown fields
        $db = $this->getDatabase();

        // Get DB column names
        $db->setQuery('SHOW COLUMNS FROM #__tassos_codesnippets');
        $dbColumns = $db->loadObjectList('Field');
        $dbColumnNames = array_keys($dbColumns);

        $params = [];

        foreach ($data as $key => $value)
        {
            // If field is not a DB column and not a special Joomla field, add to params
            // Exclude 'rules' as ACL is handled by the asset system
            // Exclude 'shortcode_display' as it's a display-only field
            if (!in_array($key, $dbColumnNames) && !in_array($key, ['tags', 'metadata', 'associations', 'rules', 'shortcode_display']))
            {
                $params[$key] = $value;
            }
        }

        // Let parent validate (this will filter out the extra fields, but we've captured them)
        $validData = parent::validate($form, $data, $group);

        if ($validData === false)
        {
            return false;
        }

        // Add params JSON
        $validData['params'] = json_encode($params);

        return $validData;
    }

    /**
     * Method to duplicate a snippet.
     *
     * @param   int  $pk  The id of the snippet to duplicate.
     *
     * @return  boolean  True on success.
     */
    public function duplicate($pk)
    {
        $table = $this->getTable();

        if (!$table->load($pk))
        {
            throw new \Exception($table->getError());
        }

        // Reset the id to create a new record
        $table->id = 0;

        // Preserve the original alias BEFORE changing the title so the copy's
        // alias increments from the same base (e.g. hello → hello-2, hello-3 …)
        // rather than being derived from the "Copy of …" title.
        $originalAlias = $table->alias;

        // Modify the title
        $table->title = Text::sprintf('COM_TASSOSCODESNIPPETS_COPY_OF', $table->title);

        // Generate a unique alias from the original alias.
        [, $table->alias] = $this->generateNewTitle($table->catid, $originalAlias, $table->title);

        // Reset state to unpublished
        $table->state = 0;

        // Reset timestamps
        $table->created     = Factory::getDate()->toSql();
        $table->created_by  = Factory::getApplication()->getIdentity()->id;
        $table->modified    = null;
        $table->modified_by = 0;

        // Store the new record
        if (!$table->store())
        {
            throw new \Exception($table->getError());
        }

        // Handle tags
        $oldTags = new TagsHelper();
        $oldTags->getTagIds($pk, 'com_tassoscodesnippets.snippet');

        if (!empty($oldTags->tags))
        {
            $newTags = new TagsHelper();
            $newTags->postStoreProcess($this->getTable(), $oldTags->tags, true);
        }

        return true;
    }

    /**
     * A protected method to get a set of ordering conditions.
     *
     * @param   Table  $table  A Table object.
     *
     * @return  array  An array of conditions to add to ordering queries.
     */
    protected function getReorderConditions($table)
    {
        return [
            $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid
        ];
    }

    /**
     * Method to get the table
     *
     * @param   string  $name    Name of the Table class
     * @param   string  $prefix  Optional prefix for the table class name
     * @param   array   $options Optional configuration array for Table object
     *
     * @return  Table|boolean Table if found, boolean false on failure
     */
    public function getTable($name = 'Snippet', $prefix = 'Administrator', $options = [])
    {
        return parent::getTable($name, $prefix, $options);
    }
}
