<?php

declare(strict_types=1);

namespace Seka\Portal\Modul\Statistiky\Services\Analyzators;

use Seka\Portal\Modul\Statistiky\Services\Import\File;
use Seka\Portal\Modul\Statistiky\Services\Analyzators\Validators\ValidatorContext;

/**
 * Class CSVDataAnalyzator
 *
 * @package Seka\Portal\Modul\Statistiky\Services\Analyzators
 */
final class CSVDataAnalyzator implements DataAnalyzatorContract
{
    /**
     * @var array
     */
    private array $validators;

    /**
     * @var array
     */
    private array $columnIndexes;

    /**
     * @var array
     */
    private array $requiredFields;

    /**
     * @var array
     */
    private array $errors;

    /**
     * @var array
     */
    private array $results = [];

    /**
     * DataAnalyzerCSV constructor.
     *
     * @param array  $required
     */
    public function __construct(array $required)
    {
        $this->validators = array();
        $this->columnIndexes = array();
        $this->errors = array();
        $this->results = array();
        $this->requiredFields = $required;
    }

    /**
     * @param string $type
     * @param        $args
     *
     * @return bool
     */
    public function analyze(string $type, $args): bool
    {
        $passed = false;

        if (method_exists($this, $type)) {
            $start = microtime(true);
            $passed = $this->$type($args);
            $time_taken = microtime(true) - $start;

            if ($passed) {
                $this->results[] = sprintf('The %s analysis has successfully passed all validations.', $type);
            }

            $this->results[] = sprintf('Time taken: %s seconds', $time_taken);
        }

        return $passed;
    }

    /**
     * @param array $data
     *
     * @return bool
     */
    protected function parseFirstRow(array $data): bool
    {
        $valid = true;
        //Clean the data
        $data = array_filter(array_map('trim', array_map('strtolower', $data)));

        //Ensure that there aren't duplicated columns
        $dupes = array_diff_key($data, array_unique($data));

        if (!empty($dupes)) {
            $this->errors[] = sprintf('The following columns are duplicated on the CSV: "%s".', implode('", "', $dupes));
            $valid = false;
        }

        //Ensure all required columns are present
        if (
            $valid
            //The number of columns is lower than the required fields, we don't need to keep checking, some columns are missing.
            && (
                count($data) < count($this->requiredFields) ||
                //The number of optional fields must match with the number of fields that are not required, otherwise something is missing.
                count(array_diff($data, $this->requiredFields)) !== (count($data) - count($this->requiredFields))
            )
        ) {
            $required = implode(',', array_diff($this->requiredFields, $data));

            if ($required) {
                $required .= '" and "categories" or "category_ids';
            } else {
                $required = 'categories" or "category_ids';
            }

            $this->errors[] = sprintf('The following columns are missing on the CSV: "%s".', $required);
            $valid = false;
        }

        if ($valid) {
            //Instantiate all the lexical validators
            foreach ($data as $key => $value) {
                $this->validators[$key] = new ValidatorContext(self::LEXICAL, $value);
                $this->columnIndexes[$key] = $value;
            }
        }

        return $valid;
    }

    /**
     * Perform the lexical analysis over the CSV
     * Iterate over the file and exit if an error is found
     *
     * @param \Seka\Portal\Modul\Statistiky\Services\Import\File $file
     * @return bool whether the file is valid (from a lexical point of view) or not
     */
    protected function lexical(File $file): bool
    {
        $file->check();

        $delimiter = self::getDelimiter($file);

        $handle = $file->open();

        //Parse the first row, instantiate all the validators
        $valid = $this->parseFirstRow(fgetcsv($handle, 0, $delimiter));
        //Number of columns specified on the header
        $numColumns = sizeOf($this->columnIndexes);
        //line number count
        $i = 1;

        while (($data = fgetcsv($handle, 0, $delimiter)) !== FALSE && $valid) {
            $errors = array();

            //For each column
            foreach ($data as $key => $value) {
                //Skip all columns without header
                if ($key >= $numColumns) {
                    break;
                }

                $value = trim($value);

                //Validate
                $errors[$this->columnIndexes[$key]] = $this->validators[$key]->validate($value);

                $valid = $valid && $errors[$this->columnIndexes[$key]];
            }

            //If any error was found, exit
            if (!$valid) {
                $filtered_errors = array_keys($errors, false);

                if (count($filtered_errors) > 0) {
                    //Store the errors founds on the current line
                    $this->errors[$i] = $filtered_errors;
                }

                break;
            }

            ++$i;
        }

        $file->close();

        return $valid;
    }

    /**
     * @param \Seka\Portal\Modul\Statistiky\Services\Import\File $file
     *
     * @return string
     */
    public static function getDelimiter(File $file): string
    {
        $line = '';

        if ($f = $file->open()) {
            $line = fgets($f);
            $file->close();
        }

        if (strpos($line, ';') !== false && strpos($line, ',') === false) {
            $delimiter = ';';
        } else if (strpos($line, ',') !== false && strpos($line, ';') === false) {
            $delimiter = ',';
        } else {
            die('Unable to find the CSV delimiter character. Make sure you use "," or ";" as delimiter and try again.');
        }

        return $delimiter;
    }

    /**
     * @return array
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * @return array
     */
    public function getResults(): array
    {
        return $this->results;
    }
}
