<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace local_coursegen\mod_settings;

use stdClass;

/**
 * Class data_settings
 *
 * Creates the database fields (columns) generated by the AI service after the database instance
 * exists. The standard module creation cannot create fields, so without this a database activity
 * has no columns and is unusable. Each definition is written through mod_data's native field API
 * (data_get_field_new -> define_field -> insert_field). The list/single/add/search templates are
 * generated lazily by mod_data the first time they are rendered (manager::get_template falls back
 * to data_generate_default_template when a stored template is empty), exactly as for a database a
 * teacher builds by hand, so no template needs to be persisted here. When no fields are present
 * the class does nothing.
 *
 * @package    local_coursegen
 * @copyright  2026 Wilber Narvaez <https://datacurso.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class data_settings extends base_settings {
    /** @var string[] Field types that store their choices, one per line, in param1. */
    private const CHOICE_TYPES = ['menu', 'multimenu', 'radiobutton', 'checkbox'];

    /** @var string[] Field types that make a sensible default sort key (identifying columns). */
    private const SORTABLE_TYPES = ['text', 'number', 'date', 'url', 'menu', 'radiobutton'];

    /**
     * Per-type param defaults applied when creating a field programmatically.
     *
     * Creating a field by code skips the edit form, so type params start empty. For most types
     * that is fine, but a few render poorly without them, so we replicate mod_data's own form
     * defaults: textarea would otherwise be a tiny box with no editor, picture would show
     * full-resolution images in lists, and a url would not be a clickable link.
     *
     * @var array<string, array<string, string>>
     */
    private const TYPE_DEFAULTS = [
        // The param2 = columns, param3 = rows, param4 = use HTML editor, param5 = max size (0 = none).
        'textarea' => ['param2' => '60', 'param3' => '35', 'param4' => '1', 'param5' => '0'],
        // The param1/param2 = list thumbnail width/height, param4/param5 = single-view width/height.
        'picture'  => ['param1' => '100', 'param2' => '200', 'param4' => '300', 'param5' => '400'],
        // The param1 = autolink (make the URL a clickable link).
        'url'      => ['param1' => '1'],
    ];

    /**
     * Create the database fields, when any were generated.
     */
    public function add_settings() {
        global $CFG, $DB;

        $fields = $this->modsettings['fields'] ?? [];
        if (empty($fields) || !is_array($fields)) {
            return;
        }

        $dataid = (int) ($this->cm->instance ?? 0);
        if (!$dataid) {
            return;
        }

        require_once($CFG->dirroot . '/mod/data/lib.php');

        $datainstance = $DB->get_record('data', ['id' => $dataid]);
        if (!$datainstance) {
            return;
        }

        $created = [];
        foreach ($fields as $fieldspec) {
            if (is_array($fieldspec)) {
                $field = $this->create_field($datainstance, $fieldspec);
                if ($field !== null) {
                    $created[] = $field;
                }
            }
        }

        $this->set_default_sort($datainstance, $created);
        $this->set_templates($datainstance);
        $this->seed_example_entries($datainstance, $created);
    }

    /**
     * Seed the AI-generated example entries (already validated service-side) as approved records.
     *
     * Each value is serialised into data_content per field type (direct insert — mod_data's
     * update_content is form-shaped for some types). An entry that errors is skipped (a bad
     * example must never break the activity). Unseedable types and unknown fields are ignored.
     *
     * @param stdClass $datainstance The database activity record.
     * @param array $created Created fields as objects with ->id, ->type, ->name.
     */
    protected function seed_example_entries(stdClass $datainstance, array $created): void {
        global $USER;

        $entries = $this->modsettings['example_entries'] ?? [];
        if (empty($entries) || !is_array($entries)) {
            return;
        }

        $byname = [];
        foreach ($created as $field) {
            $byname[\core_text::strtolower($field->name)] = $field;
        }

        foreach ($entries as $entry) {
            $values = is_array($entry) ? ($entry['values'] ?? []) : [];
            if (empty($values) || !is_array($values)) {
                continue;
            }
            try {
                $recordid = data_add_record($datainstance, 0, $USER->id, true);
                foreach ($values as $pair) {
                    if (!is_array($pair)) {
                        continue;
                    }
                    $field = $byname[\core_text::strtolower(trim((string) ($pair['field_name'] ?? '')))] ?? null;
                    if ($field !== null) {
                        $this->insert_content($recordid, $field, (string) ($pair['value'] ?? ''));
                    }
                }
            } catch (\Throwable $e) {
                debugging('local_coursegen: skipped example entry: ' . $e->getMessage(), DEBUG_DEVELOPER);
            }
        }
    }

    /**
     * Serialise one field value into a data_content row according to the field type.
     *
     * @param int $recordid The entry record id.
     * @param stdClass $field The created field (->id, ->type).
     * @param string $rawvalue The validated value as text.
     */
    protected function insert_content(int $recordid, stdClass $field, string $rawvalue): void {
        global $DB;

        $value = trim($rawvalue);
        if ($value === '' || in_array($field->type, ['file', 'picture'], true)) {
            return;
        }

        $content = (object) ['fieldid' => $field->id, 'recordid' => $recordid, 'content' => $value];
        switch ($field->type) {
            case 'number':
                // Moodle stores numbers via floatval; keep non-numeric text out of the column
                // (it would display raw and break sorting on that field).
                if (!is_numeric($value)) {
                    return;
                }
                $content->content = (string) (float) $value;
                break;
            case 'date':
                $timestamp = strtotime($value);
                if ($timestamp === false) {
                    return;
                }
                $content->content = (string) $timestamp;
                break;
            case 'menu':
            case 'radiobutton':
                // Mod_data renders a stored choice only when it exactly matches a defined option,
                // so a mismatched value would show as blank: skip it instead of storing it.
                if (!in_array($value, $field->options ?? [], true)) {
                    return;
                }
                break;
            case 'multimenu':
            case 'checkbox':
                $parts = array_filter(array_map('trim', explode(',', $value)), static fn($p) => $p !== '');
                // Same exact-match rule as menu: mod_data silently drops non-matching tokens at
                // render time, so only tokens that are defined options are worth storing.
                $parts = array_values(array_intersect($parts, $field->options ?? []));
                if (empty($parts)) {
                    return;
                }
                $content->content = implode('##', $parts);
                break;
            case 'latlong':
                $parts = array_map('trim', explode(',', $value));
                if (count($parts) < 2 || !is_numeric($parts[0]) || !is_numeric($parts[1])) {
                    return;
                }
                $content->content = $parts[0];
                $content->content1 = $parts[1];
                break;
            case 'textarea':
                $content->content1 = FORMAT_HTML;
                break;
        }
        $DB->insert_record('data_content', $content);
    }

    /**
     * Apply the AI-designed display templates (already validated service-side) to the database.
     *
     * Only the list/single columns are written, and only the non-empty ones; an absent or empty
     * template leaves the column untouched so Moodle auto-generates its default.
     *
     * @param stdClass $datainstance The database activity record.
     */
    protected function set_templates(stdClass $datainstance): void {
        global $DB;

        $templates = $this->modsettings['templates'] ?? null;
        if (empty($templates) || !is_array($templates)) {
            return;
        }

        $update = ['id' => $datainstance->id];
        foreach (['listtemplate', 'singletemplate'] as $name) {
            $value = trim((string) ($templates[$name] ?? ''));
            if ($value !== '') {
                $update[$name] = $value;
            }
        }
        if (count($update) > 1) {
            $DB->update_record('data', (object) $update);
        }
    }

    /**
     * Sort the database by its primary identifying field instead of by time added.
     *
     * Picks the first created field of a sortable type (text/number/date/url/menu/radiobutton);
     * if every field is a non-sortable type (textarea/file/picture/latlong/multimenu/checkbox)
     * the default time-added sort is kept (defaultsort stays 0). Runs after the fields exist so
     * the chosen defaultsort always points at a real field id.
     *
     * @param stdClass $datainstance The database activity record.
     * @param array $created Created fields as objects with ->id and ->type, in creation order.
     */
    protected function set_default_sort(stdClass $datainstance, array $created): void {
        global $DB;

        foreach ($created as $field) {
            if (in_array($field->type, self::SORTABLE_TYPES, true)) {
                $DB->update_record('data', (object) [
                    'id' => $datainstance->id,
                    'defaultsort' => $field->id,
                    'defaultsortdir' => 0,
                ]);
                return;
            }
        }
    }

    /**
     * Create a single field from its AI definition.
     *
     * Invalid definitions are skipped rather than failing the whole activity: an empty name or
     * type, a duplicate name (the module requires unique field names), an unknown field type, or
     * a choice field with no options.
     *
     * @param stdClass $datainstance The database activity record.
     * @param array $spec The field definition (type, name, description, required, options).
     * @return stdClass|null The created field (->id, ->type), or null when skipped/failed.
     */
    protected function create_field(stdClass $datainstance, array $spec): ?stdClass {
        $type = trim((string) ($spec['type'] ?? ''));
        $name = trim((string) ($spec['name'] ?? ''));
        if ($type === '' || $name === '') {
            return null;
        }
        if (data_fieldname_exists($name, $datainstance->id)) {
            return null;
        }

        $formdata = $this->build_formdata($type, $name, $spec);
        // A choice field with no usable options cannot be filled in -> skip it.
        if (in_array($type, self::CHOICE_TYPES, true) && trim((string) ($formdata->param1 ?? '')) === '') {
            return null;
        }

        // A single problematic field (unknown/disabled type, a DB error, an over-long name, ...)
        // must never abort the activity creation or the remaining fields: this runs after the
        // module already exists, so a thrown error would leave a half-built database.
        try {
            $field = data_get_field_new($type, $datainstance);
            $field->define_field($formdata);
            $field->insert_field();
            $options = in_array($type, self::CHOICE_TYPES, true)
                ? explode("\n", (string) $formdata->param1)
                : [];
            return (object) [
                'id' => (int) $field->field->id,
                'type' => $type,
                'name' => $name,
                'options' => $options,
            ];
        } catch (\Throwable $e) {
            debugging(
                'local_coursegen: skipped data field "' . $name . '": ' . $e->getMessage(),
                DEBUG_DEVELOPER
            );
            return null;
        }
    }

    /**
     * Build the form-data object define_field() expects from the AI field definition.
     *
     * @param string $type The Moodle field type.
     * @param string $name The (validated) field name.
     * @param array $spec The field definition.
     * @return stdClass
     */
    protected function build_formdata(string $type, string $name, array $spec): stdClass {
        $formdata = new stdClass();
        $formdata->name = $name;
        $formdata->description = trim((string) ($spec['description'] ?? ''));
        $formdata->required = !empty($spec['required']) ? 1 : 0;

        foreach (self::TYPE_DEFAULTS[$type] ?? [] as $param => $value) {
            $formdata->$param = $value;
        }

        if (in_array($type, self::CHOICE_TYPES, true)) {
            // Types menu/multimenu/radiobutton/checkbox read their options from param1, one per line.
            $formdata->param1 = implode("\n", $this->clean_options($spec['options'] ?? []));
        }

        return $formdata;
    }

    /**
     * Trim and drop blank entries from a list of choice options.
     *
     * @param mixed $options
     * @return string[]
     */
    protected function clean_options($options): array {
        if (!is_array($options)) {
            return [];
        }
        $clean = [];
        foreach ($options as $option) {
            $option = trim((string) $option);
            if ($option !== '') {
                $clean[] = $option;
            }
        }
        return $clean;
    }
}
