<?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.

namespace local_kid\local;

defined('MOODLE_INTERNAL') || die();

/**
 * Round-trip CSV service for the "Évaluations"/"Stages"/CAPP niveau/
 * commentaire import-export loop: builds the downloadable template
 * (build_template()) and parses it back on import (parse()) — the same
 * CSV_COLUMNS/legend on both sides, so the header a template ships with is
 * always exactly what the parser expects (never two independently-
 * maintained column lists). CAPP additionally reuses parse()/to_csv_row()/
 * format_field()/send() for its own second file, the "appréciation
 * d'ensemble" (one row per student — CSV_COLUMNS_APPRECIATION/
 * build_template_appreciation()), which has no équivalent in Évaluations/
 * Stages.
 *
 * Delimiter-detecting: semicolon wins ties (the ADESI/French-locale
 * convention this whole plugin follows); comma only wins if strictly more
 * frequent on the header line.
 */
class csv_import_service {

    const CSV_COLUMNS = ['code_eleve', 'nom', 'prenom', 'competence', 'niveau', 'commentaire'];

    const LEGEND_NIVEAUX = "# Niveaux possibles (à recopier exactement) : "
        . "Non observé ; Non acquis ; En cours d'acquisition ; Acquis ; Dépasse les attentes";
    const LEGEND_CONSIGNE = "# Consigne : pas de retour à la ligne dans les commentaires.";

    /**
     * Columns for the CAPP "appréciation d'ensemble" round-trip file — one
     * row per student (not per competency), entirely separate from
     * CSV_COLUMNS/build_template() above which stay unchanged for the
     * niveau/commentaire-per-competency file shared with Évaluations/Stages.
     */
    const CSV_COLUMNS_APPRECIATION = ['code_eleve', 'nom', 'prenom', 'appreciation'];

    const LEGEND_CONSIGNE_APPRECIATION = "# Consigne : pas de retour à la ligne dans l'appréciation.";

    /**
     * @return array{delimiter:string, rows:array[]} rows are assoc arrays
     *         keyed by header, plus '_line' (1-based line number in the
     *         original uploaded file, so error messages point the teacher
     *         at the right row even though blank/legend lines were skipped).
     */
    public static function parse(string $content): array {
        $content = preg_replace("/^\xEF\xBB\xBF/", '', $content); // Strip UTF-8 BOM if present.
        $content = self::normalize_embedded_newlines($content);

        $rawlines = preg_split("/\r\n|\r|\n/", trim($content));
        $lines = [];
        $linenumbers = [];
        foreach ($rawlines as $idx => $rawline) {
            $trimmed = trim($rawline);
            if ($trimmed === '' || $trimmed[0] === '#') {
                continue; // Blank lines and the '#'-prefixed legend/instruction lines are never data.
            }
            $lines[] = $rawline;
            $linenumbers[] = $idx + 1;
        }
        if (empty($lines)) {
            return ['delimiter' => ';', 'rows' => []];
        }

        $header = $lines[0];
        $semicount = substr_count($header, ';');
        $commacount = substr_count($header, ',');
        $delimiter = ($commacount > $semicount) ? ',' : ';';

        $headers = array_map('trim', str_getcsv($header, $delimiter));
        $rows = [];
        for ($i = 1; $i < count($lines); $i++) {
            $fields = str_getcsv($lines[$i], $delimiter);
            $row = [];
            foreach ($headers as $idx => $key) {
                $row[$key] = trim($fields[$idx] ?? '');
            }
            $row['_line'] = $linenumbers[$i];
            $rows[] = $row;
        }

        return ['delimiter' => $delimiter, 'rows' => $rows];
    }

    /**
     * Quote-aware single pass over the raw file: a real line break found
     * while inside a quoted field (a comment that, despite the template's
     * instruction, was saved with one anyway) is replaced with a single
     * space instead of being left to fracture that row across two "lines"
     * once split below. Doubled quotes ("") inside a quoted field are left
     * untouched, not mistaken for the field's closing quote.
     */
    private static function normalize_embedded_newlines(string $content): string {
        $result = '';
        $inquotes = false;
        $len = strlen($content);
        for ($i = 0; $i < $len; $i++) {
            $char = $content[$i];
            if ($char === '"') {
                if ($inquotes && ($i + 1) < $len && $content[$i + 1] === '"') {
                    $result .= '""';
                    $i++;
                    continue;
                }
                $inquotes = !$inquotes;
                $result .= $char;
                continue;
            }
            if ($inquotes && ($char === "\r" || $char === "\n")) {
                if ($char === "\r" && ($i + 1) < $len && $content[$i + 1] === "\n") {
                    $i++;
                }
                $result .= ' ';
                continue;
            }
            $result .= $char;
        }
        return $result;
    }

    /**
     * Quotes a field only when it needs it (contains the delimiter, a
     * quote, or a line break), doubling any internal quotes — never quotes
     * a plain value, keeping the generated file readable.
     */
    public static function format_field($value, string $delimiter = ';'): string {
        $value = (string) $value;
        if (strpbrk($value, $delimiter . "\"\r\n") !== false) {
            return '"' . str_replace('"', '""', $value) . '"';
        }
        return $value;
    }

    /**
     * @param string[] $fields
     */
    public static function to_csv_row(array $fields, string $delimiter = ';'): string {
        return implode($delimiter, array_map(fn($f) => self::format_field($f, $delimiter), $fields));
    }

    /**
     * Builds the full downloadable template: the two '#' legend lines, the
     * header, then one row per (student × competency) pair with niveau and
     * commentaire left blank — CRLF throughout, matching this plugin's
     * other CSV export (adesi_export_service).
     *
     * @param \stdClass[] $students rows with studentid, lastname, firstname
     *        (promotion_repository::get_active_students() shape).
     * @param \stdClass[] $competencies rows with libelle.
     */
    public static function build_template(array $students, array $competencies): string {
        $lines = [self::LEGEND_NIVEAUX, self::LEGEND_CONSIGNE, implode(';', self::CSV_COLUMNS)];
        foreach ($students as $student) {
            foreach ($competencies as $competency) {
                $lines[] = self::to_csv_row([
                    $student->studentid,
                    $student->lastname,
                    $student->firstname,
                    format_string($competency->libelle),
                    '',
                    '',
                ]);
            }
        }
        return implode("\r\n", $lines) . "\r\n";
    }

    /**
     * Same round-trip contract as build_template() but one row per student
     * (appréciation d'ensemble is a single entry-level field, never repeated
     * per competency) — used by the CAPP "appréciations d'ensemble" file
     * only.
     *
     * @param \stdClass[] $students rows with studentid, lastname, firstname
     *        (promotion_repository::get_active_students() shape).
     */
    public static function build_template_appreciation(array $students): string {
        $lines = [self::LEGEND_CONSIGNE_APPRECIATION, implode(';', self::CSV_COLUMNS_APPRECIATION)];
        foreach ($students as $student) {
            $lines[] = self::to_csv_row([
                $student->studentid,
                $student->lastname,
                $student->firstname,
                '',
            ]);
        }
        return implode("\r\n", $lines) . "\r\n";
    }

    public static function send(string $content, string $filename): void {
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment;filename="' . $filename . '"');
        header('Cache-Control: max-age=0');
        echo $content;
        exit;
    }
}
