<?php

declare(strict_types=1);

namespace Cin\AjaxSearch5;

use Cin\AjaxSearch5\Logger as Log;
use PDO;
use TeamTNT\TNTSearch\Indexer\TNTIndexer;
use TeamTNT\TNTSearch\TNTSearch;
use Throwable;
use Wamania\Snowball\StemmerFactory;

final class Index
{
  private TNTSearch $tnt;

  private ?TNTIndexer $indexer = null;

  private string $indexLocation = __DIR__.'/../indices';

  private ?string $cISO = null;

  private string $defaultLanguageQuery = 'SELECT
    ta.kArtikel,
    TRIM(CONCAT(
      COALESCE(ta.cArtNr, ""), " ",
      COALESCE(ta.cHAN, ""), " ",
      COALESCE(ta.cName, ""), " ",
      COALESCE(ta.cKurzBeschreibung, ""), " ",
      COALESCE(ta.cBeschreibung, ""), " ",
      COALESCE(ta.cSuchbegriffe, ""), " ",
      COALESCE(th.cName, "")
    )) \'document\'
  FROM tartikel ta
  LEFT JOIN thersteller th ON ta.kHersteller = th.kHersteller';

  private string $languageSpecificQuery = 'SELECT
    ta.kArtikel,
    TRIM(CONCAT(
      COALESCE(ta.cArtNr, ""), " ",
      COALESCE(ta.cHAN, ""), " ",
      COALESCE(tas.cName, ""), " ",
      COALESCE(tas.cKurzBeschreibung, ""), " ",
      COALESCE(tas.cBeschreibung, ""), " ",
      COALESCE(ta.cSuchbegriffe, ""), " ",
      COALESCE(th.cName, "")
    )) document
  FROM tartikel ta
  LEFT JOIN tartikelsprache tas ON ta.kArtikel = tas.kArtikel
  LEFT JOIN thersteller th ON ta.kHersteller = th.kHersteller
  WHERE tas.kSprache = :kSprache';

  private PDO $db;

  private array $cfg;

  private Plugin $p;

  public function __construct(?string $cISO = null)
  {
    if ( ! is_dir($this->indexLocation)) {
      mkdir($this->indexLocation);
    }
    $this->p  = new Plugin();
    $this->db = $this->p->getPDO();
    $cfg      = Config::create();
    if (null === $cISO) {
      $this->cISO = $this->getDefaultLanguage();
    } else {
      $this->cISO = $cISO;
    }
    $this->tnt = new TNTSearch();
    $this->tnt->loadConfig([
      'driver'   => 'mysql',
      'host'     => $cfg['host'],
      'database' => $cfg['name'],
      'username' => $cfg['user'],
      'password' => $cfg['pass'],
      'storage'  => $this->indexLocation
    ]);
    $this->cfg = $this->p->cfg();
  }

  /**
   * fills all language specific indices.
   */
  public static function primeAll(): void
  {
    Log::info('priming all indices started');
    ini_set('max_execution_time', '0');

    $languages = self::readLanguages();
    foreach ($languages as $lang) {
      $index = new self($lang['cISO']);
      $index->prime();
    }
    Log::info('priming all indices finished');
  }

  public static function dropAll(bool $logThis = true): void
  {
    $languages = self::readLanguages();
    foreach ($languages as $lang) {
      $index = new self($lang['cISO']);
      $index->drop($logThis);
    }
    rmdir(__DIR__.'/../indices');
  }

  public static function statAll(): array
  {
    $stats              = [];
    $languages          = self::readLanguages();
    $totalDocumentCount = 0;
    $totalFilesize      = 0;
    foreach ($languages as $lang) {
      $index                        = new self($lang['cISO']);
      $lang['stats']                = $index->stats();
      $stats[$lang['cNameDeutsch']] = $lang;
      $totalDocumentCount += $lang['stats']['documentCount'];
      $totalFilesize += $lang['stats']['fileSize'];
    }

    $stats['total'] = [
      'documentCount' => $totalDocumentCount,
      'fileSize'      => $totalFilesize
    ];

    return $stats;
  }

  public function stats(): array
  {
    $this->createIndexer();

    return [
      'documentCount' => (int) $this->indexer->totalDocumentsInCollection(),
      'fileSize'      => filesize($this->indexLocation.'/'.$this->cISO.'.index.sqlite3')
    ];
  }

  /**
   * initially fills a language specific index
   * expects the document count of the index to be zero.
   */
  public function prime(): void
  {
    ini_set('max_execution_time', '0');

    $this->createIndexer();

    if ($this->languageDefined() && 0 === (int) $this->indexer->totalDocumentsInCollection()) {
      Log::info('start priming index for '.$this->cISO);
      $this->indexer->run();
      Log::info('finish priming index for '.$this->cISO);
    }
  }

  /**
   * adds/updates a document to a language specific index.
   *
   * @param int $kArtikel article reference in tartikel
   */
  public function upsert(int $kArtikel): bool
  {
    $params = [
      ':kArtikel' => $kArtikel
    ];
    $sql = $this->defaultLanguageQuery;
    if ($this->isDefaultLanguage()) {
      $sql .= '
      WHERE ta.kArtikel = :kArtikel';
    } else {
      $sql = $this->languageSpecificQuery.'
      AND ta.kArtikel = :kArtikel';
      $params[':kSprache'] = $this->getkSprache();
    }
    $stmt = $this->db->prepare($sql);
    $stmt->execute($params);
    $document = $stmt->fetch(PDO::FETCH_ASSOC);
    // Logger::Debug('DOC ('.$this->cISO.') '.\gettype($document).': '.(null === \gettype($document) ? 'NULL' : print_r($document, true)));

    // $c = $this->db->prepare($sql);
    // $c->execute([
    //   ':kArtikel' => 5
    // ]);
    // Logger::Info('C '.print_r($c->fetch(PDO::FETCH_ASSOC), true));

    if (false === $document) {
      Log::warn('cannot get document for '.$kArtikel.' with SQL: '.$sql.' and params: '.json_encode($params));

      return false;
    }
    if ( ! $this->isDefaultLanguage()) {
      if ('' === trim($document['document'])) {
        return true;
      }
    }
    Log::info('upserting article '.$kArtikel.' for '.$this->cISO.' with document: '.json_encode($document));
    $this->createIndexer();
    $this->indexer->update($kArtikel, $document);

    return true;
  }

  /**
   * removes a document from the language specific index.
   *
   * * @param int $kArtikel article reference in tartikel
   */
  public function remove(int $kArtikel): void
  {
    Log::info('deleting '.$kArtikel.'from '.$this->cISO);
    $this->createIndexer();
    $this->indexer->delete($kArtikel);
  }

  /**
   * drops language specific index.
   */
  public function drop(bool $logThis = true): void
  {
    $indexFile = __DIR__.'/../indices/'.$this->cISO.'.index.sqlite3';
    if (file_exists($indexFile)) {
      if ($logThis) {
        Log::info('droping index for '.$this->cISO);
      }
      $this->indexer = null;
      unlink($indexFile);
    }
  }

  public static function readLanguages(): array
  {
    $cfg = Config::create();
    $db  = new PDO(
      'mysql:dbname='.$cfg['name'].';host='.$cfg['host'].';charset=utf8mb4',
      $cfg['user'],
      $cfg['pass'],
      [
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES   => false,
        PDO::ATTR_STRINGIFY_FETCHES  => false
      ]
    );
    $minor = (int) explode('.', APPLICATION_VERSION)[1];
    if (0 === $minor) {
      $stmt = $db->prepare('SELECT * FROM tsprache');
    } else {
      $stmt = $db->prepare('SELECT * FROM tsprache WHERE active=1');
    }
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }

  private function createIndexer(): void
  {
    if (null === $this->indexer && $this->languageDefined()) {
      $tokenizer = new StripTagsTokenizer($this->cfg['minchars'], $this->cfg['minchars']);
      $indexName = $this->cISO.'.index.sqlite3';
      $stemmer   = StemmerFactory::create($this->cISO);

      try {
        $this->tnt->selectIndex($indexName);
      } catch (Throwable $th) {
        $this->tnt->createIndex($indexName);
        $this->tnt->selectIndex($indexName);
      }
      $this->indexer = $this->tnt->getIndex();
      $this->indexer->setTokenizer($tokenizer);
      $this->indexer->setStemmer($stemmer);
      $this->indexer->query($this->chooseLangQuery());
      $this->indexer->setPrimaryKey('kArtikel');
    }
  }

  private function languageDefined(?string $cISO = null): bool
  {
    if (null === $cISO) {
      $cISO = $this->cISO;
    }
    $list = $this->readLanguages();

    return 0 !== \count(array_filter($list, static function ($lang) use ($cISO) {
      return $lang['cISO'] === $cISO;
    }));
  }

  private function chooseLangQuery(): string
  {
    $selectChildren = $this->p->getConfig('includechildren');
    $query          = '';
    if ($this->isDefaultLanguage()) {
      $query = $this->defaultLanguageQuery;
      if ( ! $selectChildren) {
        $query .= '
        WHERE ta.kVAterArtikel = 0';
      }
    } else {
      $ksprache = (string) $this->getkSprache();
      $query    = str_replace(':kSprache', $ksprache, $this->languageSpecificQuery);
      if ( ! $selectChildren) {
        $query .= '
        AND ta.kVaterArtikel = 0';
      }
    }

    return $query;
  }

  private function getDefaultLanguage(): string
  {
    $list = $this->readLanguages();
    $lang = array_values(array_filter($list, static function ($entry) {
      return 'Y' === $entry['cShopStandard'];
    }))[0];

    return $lang['cISO'];
  }

  private function isDefaultLanguage($cISO = null)
  {
    if (null === $cISO) {
      $cISO = $this->cISO;
    }

    return $cISO === $this->getDefaultLanguage();
  }

  private function getkSprache(?string $cISO = null): int
  {
    if (null === $cISO) {
      $cISO = $this->cISO;
    }
    $list = $this->readLanguages();
    $lang = array_values(array_filter($list, static function ($lang) use ($cISO) {
      return $lang['cISO'] === $cISO;
    }));

    return $lang[0]['kSprache'];
  }
}
