<?php

namespace Storeplugin\PDFInvoice\Framework\Forms;

defined('ABSPATH') || exit;

use Storeplugin\PDFInvoice\Framework\Fields\FieldRenderer;
use Storeplugin\PDFInvoice\Framework\Fields\FieldValidator;
use Storeplugin\PDFInvoice\Framework\Fields\Types\RepeaterField;
use Storeplugin\PDFInvoice\Framework\Contracts\TermInterface;
use Storeplugin\PDFInvoice\Framework\Traits\SupportsRegisteredTermMeta;
use Storeplugin\PDFInvoice\Framework\Utils\FieldState;
use Storeplugin\PDFInvoice\Config;
use Storeplugin\PDFInvoice\Asset;

/**
 * Term class to handle custom fields for taxonomy terms.
 *
 * @package Storeplugin\PDFInvoice\Framework\Forms
 * @since 1.0.0
 */
class Term implements TermInterface
{
    use SupportsRegisteredTermMeta;

    /**
     * Unique identifier for this term meta group
     *
     * @var string
     */
    protected $id;

    /**
     * Taxonomies to show the fields on
     *
     * @var array
     */
    protected $taxonomies;

    /**
     * Option group prefix for form field names
     *
     * @var string
     */
    protected $optionKey;

    /**
     * Sections containing field definitions
     *
     * @var array
     */
    protected $sections = [];

    /**
     * Constructor.
     *
     * @param string       $id         Unique identifier for this term meta group
     * @param string|array $taxonomies Taxonomies to show the fields on
     * @param string       $optionKey  Prefix for form field names
     */
    private function __construct($id, $taxonomies, string $optionKey)
    {
        $this->id = $id;
        $this->taxonomies = (array) $taxonomies;
        $optionKey = \sanitize_key($optionKey);
        if ($optionKey === '') {
            throw new \InvalidArgumentException('Term optionKey cannot be empty. Pass a unique optionKey for each term form.');
        }

        $this->optionKey = $optionKey;
    }

    /**
     * Create a new term meta instance
     *
     * @param string       $id         Unique identifier for this term meta group
     * @param string|array $taxonomies Taxonomies to show the fields on
     * @param string       $optionKey  Prefix for form field names
     *
     * @return TermInterface
     */
    public static function create($id, $taxonomies, string $optionKey): TermInterface
    {
        return new self($id, $taxonomies, $optionKey);
    }

    /**
     * Add a section of fields
     *
     * @param string $title  Section title
     * @param array  $fields Array of field definitions
     * @return self
     */
    public function addSection($title, $fields): self
    {
        $this->sections[] = [
            'title'  => $title,
            'fields' => $fields,
        ];

        return $this;
    }

    /**
     * Get the configured taxonomies for REST meta registration.
     *
     * @internal Used by framework REST registrars.
     * @return array<int, string>
     */
    public function restTaxonomies(): array
    {
        return array_values(array_unique(array_filter(array_map('strval', $this->taxonomies))));
    }

    /**
     * Get all configured sections for REST field registration.
     *
     * @internal Used by framework REST registrars.
     * @return array<int, array>
     */
    public function restSections(): array
    {
        return $this->sections;
    }

    /**
     * Sanitize field value.
     *
     * @internal WordPress callback method - do not call directly
     * @param mixed  $value
     * @param string $type
     * @return mixed
     */
    public function sanitizeField($value, string $type = 'text')
    {
        if (is_array($value)) {
            return array_map(function ($item) use ($type) {
                return $this->sanitizeField($item, $type);
            }, $value);
        }

        if ($type === 'wysiwyg') {
            return \wp_kses_post($value);
        }

        if ($type === 'textarea') {
            return \sanitize_textarea_field($value);
        }

        return \sanitize_text_field($value);
    }

    /**
     * Sanitize repeater rows with per-sub-field type awareness.
     *
     * @param array $rows                     Raw rows from POST data.
     * @param array $subFieldDefs             Sub-field definitions from the repeater config.
     * @param array $currentRows              Previously saved repeater rows keyed by row index.
     * @param bool  $allowLockedSubmittedValues Whether locked fields may reuse the submitted value when no
     *                                          current row exists. Used for register_meta() re-sanitization.
     * @return array Sanitized rows.
     */
    protected function sanitizeRepeaterRows(
        array $rows,
        array $subFieldDefs,
        array $currentRows = [],
        bool $allowLockedSubmittedValues = false
    ): array
    {
        $sanitized = [];
        $currentRowMap = $this->getRepeaterCurrentRowsByKey($currentRows);

        foreach ($rows as $rowIndex => $row) {
            if (!is_array($row)) {
                continue;
            }

            $sanitizedRow = [];
            $currentRow = $this->resolveCurrentRepeaterRow($row, $rowIndex, $currentRowMap);
            $rowNum = (int) $rowIndex + 1;

            foreach ($subFieldDefs as $subField) {
                if (!is_array($subField)) {
                    continue;
                }

                $subId = \sanitize_key((string) ($subField['id'] ?? ''));
                if ($subId === '') {
                    continue;
                }

                $subType = (string) ($subField['type'] ?? 'text');

                if ($this->isFieldLocked($subField)) {
                    $lockedValue = $this->resolveLockedRepeaterSubFieldValue(
                        $subField,
                        $row,
                        $currentRow,
                        $allowLockedSubmittedValues
                    );
                    if (!$lockedValue['has_value']) {
                        continue;
                    }

                    if (!empty($lockedValue['should_validate'])) {
                        $validationField = $subField;
                        $validationField['id'] = $subId . '_row_' . $rowNum;
                        $validationField['label'] = ($subField['label'] ?? $subId) . ' (Row ' . $rowNum . ')';
                        if (array_key_exists($subId, $currentRow)) {
                            $validationField['_current_value'] = $currentRow[$subId];
                        }
                        $validationField['_input_context'] = $row;

                        if (!FieldValidator::validate($lockedValue['value'], $subType, $validationField)) {
                            continue;
                        }
                    }

                    $sanitizedRow[$subId] = $lockedValue['value'];
                    continue;
                }

                if (array_key_exists($subId, $row)) {
                    if ($subType === 'repeater' && is_array($row[$subId]) && !empty($subField['fields'])) {
                        $currentNestedRows = [];
                        if (isset($currentRow[$subId]) && is_array($currentRow[$subId])) {
                            $currentNestedRows = $currentRow[$subId];
                        }

                        $sanitizedValue = $this->sanitizeRepeaterRows(
                            $row[$subId],
                            $subField['fields'],
                            $currentNestedRows,
                            $allowLockedSubmittedValues
                        );
                    } else {
                        $sanitizedValue = $this->sanitizeField($row[$subId], $subType);
                    }

                    $validationField = $subField;
                    $validationField['id'] = $subId . '_row_' . $rowNum;
                    $validationField['label'] = ($subField['label'] ?? $subId) . ' (Row ' . $rowNum . ')';
                    if (array_key_exists($subId, $currentRow)) {
                        $validationField['_current_value'] = $currentRow[$subId];
                    }
                    $validationField['_input_context'] = $row;

                    if (FieldValidator::validate($sanitizedValue, $subType, $validationField)) {
                        $sanitizedRow[$subId] = $sanitizedValue;
                    } elseif (array_key_exists($subId, $currentRow)) {
                        $sanitizedRow[$subId] = $currentRow[$subId];
                    }
                } elseif (in_array($subType, ['checkbox', 'switch'], true)) {
                    $sanitizedRow[$subId] = isset($subField['options']) && is_array($subField['options']) ? [] : '0';
                }
            }

            if (!empty($sanitizedRow)) {
                $sanitized[] = $sanitizedRow;
            }
        }

        return $sanitized;
    }

    /**
     * Resolve the value to keep for a locked top-level term meta field.
     *
     * @param array $field
     * @param array $input
     * @param mixed $currentValue
     * @return array{has_value:bool,value:mixed,should_validate:bool}
     */
    protected function resolveLockedFieldValue(array $field, array $input, $currentValue): array
    {
        if ($currentValue !== null) {
            return ['has_value' => true, 'value' => $currentValue, 'should_validate' => false];
        }

        $id = \sanitize_key((string) ($field['id'] ?? ''));
        if ($id === '') {
            return ['has_value' => false, 'value' => null, 'should_validate' => false];
        }

        $type = (string) ($field['type'] ?? 'text');
        if ($type === 'repeater' && array_key_exists('default', $field)) {
            return [
                'has_value' => true,
                'value' => $this->sanitizeValueForFieldDefinition($field, $field['default'], true),
                'should_validate' => false,
            ];
        }

        $isDisabled = FieldState::isTruthy($field['disabled'] ?? false);
        if (!$isDisabled && array_key_exists($id, $input)) {
            return [
                'has_value' => true,
                'value' => $this->sanitizeValueForFieldDefinition($field, $input[$id]),
                'should_validate' => true,
            ];
        }

        if (array_key_exists('default', $field)) {
            return [
                'has_value' => true,
                'value' => $this->sanitizeValueForFieldDefinition($field, $field['default']),
                'should_validate' => false,
            ];
        }

        $implicitValue = FieldState::getImplicitValue(array_merge($field, ['_input_context' => $input]));
        $implicitValue['should_validate'] = false;

        return $implicitValue;
    }

    /**
     * Resolve the value to preserve for a locked repeater sub-field.
     *
     * @param array $field
     * @param array $row
     * @param array $currentRow
     * @param bool  $allowSubmittedValue
     * @return array{has_value:bool,value:mixed,should_validate:bool}
     */
    protected function resolveLockedRepeaterSubFieldValue(
        array $field,
        array $row,
        array $currentRow,
        bool $allowSubmittedValue = false
    ): array {
        $id = \sanitize_key((string) ($field['id'] ?? ''));
        if ($id === '') {
            return ['has_value' => false, 'value' => null, 'should_validate' => false];
        }

        if (array_key_exists($id, $currentRow)) {
            return ['has_value' => true, 'value' => $currentRow[$id], 'should_validate' => false];
        }

        $isDisabled = FieldState::isTruthy($field['disabled'] ?? false);
        if (($allowSubmittedValue || !$isDisabled) && array_key_exists($id, $row)) {
            return [
                'has_value' => true,
                'value' => $this->sanitizeValueForFieldDefinition($field, $row[$id], $allowSubmittedValue),
                'should_validate' => !$isDisabled,
            ];
        }

        if (array_key_exists('default', $field)) {
            return [
                'has_value' => true,
                'value' => $this->sanitizeValueForFieldDefinition($field, $field['default'], $allowSubmittedValue),
                'should_validate' => false,
            ];
        }

        $implicitValue = FieldState::getImplicitValue(array_merge($field, ['_input_context' => $row]));
        $implicitValue['should_validate'] = false;

        return $implicitValue;
    }

    /**
     * Sanitize a value using the given field definition.
     *
     * @param array $field
     * @param mixed $value
     * @param bool  $allowLockedSubmittedValues
     * @return mixed
     */
    protected function sanitizeValueForFieldDefinition(array $field, $value, bool $allowLockedSubmittedValues = false)
    {
        $type = (string) ($field['type'] ?? 'text');

        if ($type === 'repeater' && is_array($value) && !empty($field['fields'])) {
            return $this->sanitizeRepeaterRows(
                $value,
                $this->getSanitizableRepeaterSubFields($field),
                [],
                $allowLockedSubmittedValues
            );
        }

        return $this->sanitizeField($value, $type);
    }

    /**
     * Apply repeater-level lock semantics to child field definitions for save-time sanitization.
     *
     * @param array $field
     * @return array
     */
    protected function getSanitizableRepeaterSubFields(array $field): array
    {
        $subFields = isset($field['fields']) && is_array($field['fields']) ? $field['fields'] : [];
        if ($subFields === []) {
            return [];
        }

        $isDisabled = FieldState::isTruthy($field['disabled'] ?? false);
        $isReadonly = !$isDisabled && FieldState::isTruthy($field['readonly'] ?? false);
        if (!$isDisabled && !$isReadonly) {
            return $subFields;
        }

        foreach ($subFields as $index => $subField) {
            if (!is_array($subField)) {
                continue;
            }

            $subFields[$index] = FieldState::applyRepeaterLockToSubField($subField, $isDisabled, $isReadonly);
        }

        return $subFields;
    }

    /**
     * Determine whether the field should be treated as immutable during save.
     *
     * @param array $field
     * @return bool
     */
    protected function isFieldLocked(array $field): bool
    {
        return FieldState::isLocked($field);
    }

    /**
     * Build a map of current repeater rows keyed by their stable row token.
     *
     * @param array $currentRows
     * @return array<string, array>
     */
    private function getRepeaterCurrentRowsByKey(array $currentRows): array
    {
        $rowMap = [];

        foreach ($currentRows as $rowIndex => $currentRow) {
            if (!is_array($currentRow)) {
                continue;
            }

            $rowMap[RepeaterField::buildRowKey($currentRow, (string) $rowIndex)] = $currentRow;
        }

        return $rowMap;
    }

    /**
     * Resolve the pre-save repeater row that matches the submitted row.
     *
     * @param array               $row
     * @param int|string          $rowIndex
     * @param array<string,array> $currentRowMap
     * @return array
     */
    private function resolveCurrentRepeaterRow(array $row, $rowIndex, array $currentRowMap): array
    {
        $rowKey = isset($row[RepeaterField::ROW_KEY_FIELD]) && is_scalar($row[RepeaterField::ROW_KEY_FIELD])
            ? (string) $row[RepeaterField::ROW_KEY_FIELD]
            : '';

        if ($rowKey !== '' && isset($currentRowMap[$rowKey]) && is_array($currentRowMap[$rowKey])) {
            return $currentRowMap[$rowKey];
        }

        $fallbackKey = RepeaterField::buildRowKey($row, (string) $rowIndex);

        return isset($currentRowMap[$fallbackKey]) && is_array($currentRowMap[$fallbackKey])
            ? $currentRowMap[$fallbackKey]
            : [];
    }

    /**
     * Render fields on "Add New Term" form
     *
     * @internal WordPress callback method - do not call directly
     * @param string $taxonomy Current taxonomy slug
     */
    public function renderAddFormFields($taxonomy)
    {
        Asset::registerFrameworkAssets();
        \wp_enqueue_style(Config::asset('framework'));
        \wp_enqueue_script(Config::asset('options'));

        \wp_nonce_field($this->id . '_action', $this->id . '_nonce');

        echo '<div class="form-field term-meta-wrap">';

        foreach ($this->sections as $section) {
            if (!empty($section['title'])) {
                echo '<h4 class="nxtf-term-meta-title">' . \esc_html($section['title']) . '</h4>';
            }

            echo '<div class="nxtf-space-y-4 nxtf-term-fields">';
            foreach ($section['fields'] as $field) {
                // Output is pre-escaped by FieldRenderer
                FieldRenderer::render($field, null, $this->optionKey);
            }
            echo '</div>';

            if (next($this->sections)) {
                echo '<hr class="nxtf-section-separator">';
            }
        }

        echo '</div>';
    }

    /**
     * Render fields on "Edit Term" form
     *
     * @internal WordPress callback method - do not call directly
     * @param \WP_Term $term     Current term object
     * @param string   $taxonomy Current taxonomy slug
     */
    public function renderEditFormFields($term, $taxonomy)
    {
        Asset::registerFrameworkAssets();
        \wp_enqueue_style(Config::asset('framework'));
        \wp_enqueue_script(Config::asset('options'));

        \wp_nonce_field($this->id . '_action', $this->id . '_nonce');

        foreach ($this->sections as $section) {
            echo '<tr class="form-field term-meta-wrap">';
            echo '<th scope="row">';

            if (!empty($section['title'])) {
                echo '<label class="nxtf-term-meta-title">' . \esc_html($section['title']) . '</label>';
            }

            echo '</th>';
            echo '<td>';

            echo '<div class="nxtf-space-y-4 nxtf-term-fields">';
            foreach ($section['fields'] as $field) {
                $id = \sanitize_key((string) ($field['id'] ?? ''));
                if ($id === '') { continue; }
                $value = \metadata_exists('term', $term->term_id, $id)
                    ? \get_term_meta($term->term_id, $id, true)
                    : null;
                FieldRenderer::render($field, $value, $this->optionKey);
            }
            echo '</div>';

            echo '</td>';
            echo '</tr>';
        }
    }

    /**
     * Save term meta data
     *
     * @internal WordPress callback method - do not call directly
     * @param int $termId Term ID
     */
    public function saveTermMeta($termId)
    {
        // Security checks
        $nonce = isset($_POST[$this->id . '_nonce'])
            ? \sanitize_text_field(\wp_unslash($_POST[$this->id . '_nonce']))
            : '';

        if (!$nonce || !\wp_verify_nonce($nonce, $this->id . '_action')) {
            return;
        }

        // Check permissions
        if (!current_user_can('edit_term', $termId)) {
            return;
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized per-field via sanitizeField() in the loop below.
        $input = isset($_POST[$this->optionKey]) && is_array($_POST[$this->optionKey]) ? \wp_unslash($_POST[$this->optionKey]) : [];
        FieldValidator::clearErrors();

        // Save fields
        foreach ($this->sections as $section) {
            foreach ($section['fields'] as $field) {
                $fieldId = \sanitize_key((string) ($field['id'] ?? ''));
                if ($fieldId === '') { continue; }
                $fieldType = $field['type'] ?? 'text';
                $currentValue = \metadata_exists('term', $termId, $fieldId)
                    ? \get_term_meta($termId, $fieldId, true)
                    : null;

                if ($this->isFieldLocked($field)) {
                    $lockedValue = $this->resolveLockedFieldValue($field, $input, $currentValue);
                    if (!$lockedValue['has_value']) {
                        continue;
                    }

                    if (!empty($lockedValue['should_validate'])) {
                        $validationField = $field;
                        if ($currentValue !== null) {
                            $validationField['_current_value'] = $currentValue;
                        }
                        $validationField['_input_context'] = $input;

                        if (!FieldValidator::validate($lockedValue['value'], (string) $fieldType, $validationField)) {
                            continue;
                        }
                    }

                    $this->updateRegisteredTermMeta($termId, $field, $lockedValue['value'], $currentValue);
                    continue;
                }

                if (array_key_exists($fieldId, $input)) {
                    if ($fieldType === 'repeater' && is_array($input[$fieldId]) && !empty($field['fields'])) {
                        $value = $this->sanitizeRepeaterRows(
                            $input[$fieldId],
                            $field['fields'],
                            is_array($currentValue) ? $currentValue : []
                        );
                    } else {
                        $value = $this->sanitizeField($input[$fieldId], $fieldType);
                    }

                    $validationField = $field;
                    if ($currentValue !== null) {
                        $validationField['_current_value'] = $currentValue;
                    }
                    $validationField['_input_context'] = $input;

                    if (!FieldValidator::validate($value, (string) $fieldType, $validationField)) {
                        continue;
                    }

                    $this->updateRegisteredTermMeta($termId, $field, $value, $currentValue);
                } elseif (in_array($fieldType, ['checkbox', 'switch'], true) && array_key_exists('_nxtf_rendered_' . $fieldId, $input)) {
                    // Handle unchecked checkboxes/switches — save explicit off-state
                    $offValue = (isset($field['options']) && is_array($field['options'])) ? [] : '0';
                    $this->updateRegisteredTermMeta($termId, $field, $offValue, $currentValue);
                } elseif ($fieldType === 'repeater' && array_key_exists('_nxtf_rendered_' . $fieldId, $input)) {
                    $this->updateRegisteredTermMeta($termId, $field, [], $currentValue);
                }
            }
        }

        FieldValidator::clearErrors();
    }

    /**
     * Register the term meta fields with WordPress
     */
    public function render(): void
    {
        // Register meta keys
        \add_action('init', [$this, 'registerMeta']);

        // Add hooks for each taxonomy
        foreach ($this->taxonomies as $taxonomy) {
            // Add form fields
            \add_action($taxonomy . '_add_form_fields', [$this, 'renderAddFormFields']);
            \add_action($taxonomy . '_edit_form_fields', [$this, 'renderEditFormFields'], 10, 2);

            // Save term meta
            \add_action('created_' . $taxonomy, [$this, 'saveTermMeta']);
            \add_action('edited_' . $taxonomy, [$this, 'saveTermMeta']);
        }
    }
}
