<?php
namespace Domain\Article;

/**
 * Repository odpowiedzialny za dostep do danych artykulow
 */
class ArticleRepository
{
    private const MAX_PER_PAGE = 100;

    private $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    /**
     * Pobiera artykul po ID wraz z tlumaczeniami, obrazami, plikami i powiazanymi stronami
     */
    public function find(int $articleId): ?array
    {
        $article = $this->db->get('pp_articles', '*', ['id' => $articleId]);

        if (!$article) {
            return null;
        }

        $results = $this->db->select('pp_articles_langs', '*', ['article_id' => $articleId]);
        if (is_array($results)) {
            foreach ($results as $row) {
                $article['languages'][$row['lang_id']] = $row;
            }
        }

        $article['images'] = $this->db->select('pp_articles_images', '*', [
            'article_id' => $articleId,
            'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
        ]);
        try {
            $article['files'] = $this->db->select('pp_articles_files', '*', [
                'article_id' => $articleId,
                'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
            ]);
        } catch (\Throwable $e) {
            // Fallback for instances where pp_articles_files does not yet have "o" column.
            $article['files'] = $this->db->select('pp_articles_files', '*', ['article_id' => $articleId]);
        }
        $article['pages'] = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]);

        return $article;
    }

    /**
     * Zapisuje artykul (tworzy nowy lub aktualizuje istniejacy).
     * Zwraca ID artykulu.
     */
    public function save(int $articleId, array $data, int $userId): int
    {
        if (!$articleId) {
            return $this->createArticle($data, $userId);
        }

        return $this->updateArticle($articleId, $data, $userId);
    }

    private function createArticle(array $data, int $userId): int
    {
        $this->db->insert('pp_articles', $this->buildArticleRow($data, $userId, true));

        $id = $this->db->id();

        if (!$id) {
            return 0;
        }

        $this->saveTranslations($id, $data, true);
        $this->savePages($id, $data['pages'] ?? null, true);
        $this->assignTempFiles($id);
        $this->assignTempImages($id);
        $this->applyGalleryOrderIfProvided($id, $data);
        $this->applyFilesOrderIfProvided($id, $data);

        \Shared\Helpers\Helpers::htacces();
        \Shared\Helpers\Helpers::delete_dir('../temp/');

        return (int)$id;
    }

    private function updateArticle(int $articleId, array $data, int $userId): int
    {
        $this->db->update('pp_articles', $this->buildArticleRow($data, $userId, false), [
            'id' => $articleId
        ]);

        $this->saveTranslations($articleId, $data, false);
        $this->savePages($articleId, $data['pages'] ?? null, false);
        $this->assignTempFiles($articleId);
        $this->assignTempImages($articleId);
        $this->applyGalleryOrderIfProvided($articleId, $data);
        $this->applyFilesOrderIfProvided($articleId, $data);
        $this->deleteMarkedImages($articleId);
        $this->deleteMarkedFiles($articleId);

        \Shared\Helpers\Helpers::htacces();
        \Shared\Helpers\Helpers::delete_dir('../temp/');

        return $articleId;
    }

    private function buildArticleRow(array $data, int $userId, bool $isNew): array
    {
        $row = [
            'show_title'             => $this->isCheckedValue($data['show_title'] ?? null) ? 1 : 0,
            'show_date_add'          => $this->isCheckedValue($data['show_date_add'] ?? null) ? 1 : 0,
            'show_date_modify'       => $this->isCheckedValue($data['show_date_modify'] ?? null) ? 1 : 0,
            'date_modify'            => date('Y-m-d H:i:s'),
            'modify_by'              => $userId,
            'layout_id'              => !empty($data['layout_id']) ? (int)$data['layout_id'] : null,
            'status'                 => $this->isCheckedValue($data['status'] ?? null) ? 1 : 0,
            'repeat_entry'           => $this->isCheckedValue($data['repeat_entry'] ?? null) ? 1 : 0,
            'social_icons'           => $this->isCheckedValue($data['social_icons'] ?? null) ? 1 : 0,
            'show_table_of_contents' => $this->isCheckedValue($data['show_table_of_contents'] ?? null) ? 1 : 0,
        ];

        if ($isNew) {
            $row['date_add'] = date('Y-m-d H:i:s');
        }

        return $row;
    }

    private function buildLangRow($langId, array $data): array
    {
        return [
            'lang_id'             => $langId,
            'title'               => ($data['title'][$langId] ?? '') != '' ? $data['title'][$langId] : null,
            'main_image'          => ($data['main_image'][$langId] ?? '') != '' ? $data['main_image'][$langId] : null,
            'entry'               => ($data['entry'][$langId] ?? '') != '' ? $data['entry'][$langId] : null,
            'text'                => ($data['text'][$langId] ?? '') != '' ? $data['text'][$langId] : null,
            'table_of_contents'   => ($data['table_of_contents'][$langId] ?? '') != '' ? $data['table_of_contents'][$langId] : null,
            'meta_title'          => ($data['meta_title'][$langId] ?? '') != '' ? $data['meta_title'][$langId] : null,
            'meta_description'    => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$langId] : null,
            'meta_keywords'       => ($data['meta_keywords'][$langId] ?? '') != '' ? $data['meta_keywords'][$langId] : null,
            'seo_link'            => \Shared\Helpers\Helpers::seo($data['seo_link'][$langId] ?? '') != '' ? \Shared\Helpers\Helpers::seo($data['seo_link'][$langId]) : null,
            'noindex'             => $this->isCheckedValue($data['noindex'][$langId] ?? null) ? 1 : 0,
            'copy_from'           => ($data['copy_from'][$langId] ?? '') != '' ? $data['copy_from'][$langId] : null,
            'block_direct_access' => $this->isCheckedValue($data['block_direct_access'][$langId] ?? null) ? 1 : 0,
        ];
    }

    private function applyGalleryOrderIfProvided(int $articleId, array $data): void
    {
        $order = trim((string)($data['gallery_order'] ?? ''));
        if ($order === '') {
            return;
        }

        $this->saveGalleryOrder($articleId, $order);
    }

    private function applyFilesOrderIfProvided(int $articleId, array $data): void
    {
        $order = trim((string)($data['files_order'] ?? ''));
        if ($order === '') {
            return;
        }

        $this->saveFilesOrder($articleId, $order);
    }

    private function saveTranslations(int $articleId, array $data, bool $isNew): void
    {
        $titles = $data['title'] ?? [];

        foreach ($titles as $langId => $val) {
            $langRow = $this->buildLangRow($langId, $data);

            if ($isNew) {
                $langRow['article_id'] = $articleId;
                $this->db->insert('pp_articles_langs', $langRow);
            } else {
                $translationId = $this->db->get('pp_articles_langs', 'id', [
                    'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
                ]);

                if ($translationId) {
                    $this->db->update('pp_articles_langs', $langRow, ['id' => $translationId]);
                } else {
                    $langRow['article_id'] = $articleId;
                    $this->db->insert('pp_articles_langs', $langRow);
                }
            }
        }
    }

    private function savePages(int $articleId, $pages, bool $isNew): void
    {
        if (!$isNew) {
            $notIn = [0];

            if (is_array($pages)) {
                foreach ($pages as $page) {
                    $notIn[] = $page;
                }
            } elseif ($pages) {
                $notIn[] = $pages;
            }

            $this->db->delete('pp_articles_pages', [
                'AND' => ['article_id' => $articleId, 'page_id[!]' => $notIn]
            ]);

            $existingPages = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]);

            if (!is_array($pages)) {
                $pages = [$pages];
            }

            $pages = array_diff($pages, is_array($existingPages) ? $existingPages : []);
        } else {
            if (!is_array($pages)) {
                $pages = $pages ? [$pages] : [];
            }
        }

        if (is_array($pages)) {
            foreach ($pages as $page) {
                $order = $this->maxPageOrder() + 1;

                $this->db->insert('pp_articles_pages', [
                    'article_id' => $articleId,
                    'page_id'    => (int)$page,
                    'o'          => $order,
                ]);
            }
        }
    }

    private function assignTempFiles(int $articleId): void
    {
        $results = $this->db->select('pp_articles_files', '*', ['article_id' => null]);

        if (!is_array($results)) {
            return;
        }

        $created = false;
        $dir = '/upload/article_files/article_' . $articleId;

        foreach ($results as $row) {
            $newFileName = str_replace('/upload/article_files/tmp', $dir, $row['src']);

            if (file_exists('..' . $row['src'])) {
                if (!is_dir('../' . $dir) && $created !== true) {
                    if (mkdir('../' . $dir, 0755, true)) {
                        $created = true;
                    }
                }
                rename('..' . $row['src'], '..' . $newFileName);
            }

            $this->db->update('pp_articles_files', [
                'src' => $newFileName,
                'article_id' => $articleId,
            ], ['id' => $row['id']]);
        }
    }

    private function assignTempImages(int $articleId): void
    {
        $results = $this->db->select('pp_articles_images', '*', ['article_id' => null]);

        if (!is_array($results)) {
            return;
        }

        $created = false;
        $dir = '/upload/article_images/article_' . $articleId;

        foreach ($results as $row) {
            $newFileName = str_replace('/upload/article_images/tmp', $dir, $row['src']);

            if (file_exists('../' . $newFileName)) {
                $ext = strrpos($newFileName, '.');
                $fileNameA = substr($newFileName, 0, $ext);
                $fileNameB = substr($newFileName, $ext);

                $count = 1;
                while (file_exists('../' . $fileNameA . '_' . $count . $fileNameB)) {
                    $count++;
                }

                $newFileName = $fileNameA . '_' . $count . $fileNameB;
            }

            if (file_exists('..' . $row['src'])) {
                if (!is_dir('../' . $dir) && $created !== true) {
                    if (mkdir('../' . $dir, 0755, true)) {
                        $created = true;
                    }
                }
                rename('..' . $row['src'], '..' . $newFileName);
            }

            $this->db->update('pp_articles_images', [
                'src' => $newFileName,
                'article_id' => $articleId,
            ], ['id' => $row['id']]);
        }
    }

    private function deleteMarkedImages(int $articleId): void
    {
        $results = $this->db->select('pp_articles_images', '*', [
            'AND' => ['article_id' => $articleId, 'to_delete' => 1]
        ]);

        if (is_array($results)) {
            foreach ($results as $row) {
                if (file_exists('../' . $row['src'])) {
                    unlink('../' . $row['src']);
                }
            }
        }

        $this->db->delete('pp_articles_images', [
            'AND' => ['article_id' => $articleId, 'to_delete' => 1]
        ]);
    }

    private function deleteMarkedFiles(int $articleId): void
    {
        $results = $this->db->select('pp_articles_files', '*', [
            'AND' => ['article_id' => $articleId, 'to_delete' => 1]
        ]);

        if (is_array($results)) {
            foreach ($results as $row) {
                if (file_exists('../' . $row['src'])) {
                    unlink('../' . $row['src']);
                }
            }
        }

        $this->db->delete('pp_articles_files', [
            'AND' => ['article_id' => $articleId, 'to_delete' => 1]
        ]);
    }

    private function maxPageOrder(): int
    {
        $max = $this->db->max('pp_articles_pages', 'o');
        return $max ? (int)$max : 0;
    }

    /**
     * Archiwizuje artykul (ustawia status = -1).
     */
    public function archive(int $articleId): bool
    {
        $result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]);
        return (bool)$result;
    }

    /**
     * Przywraca artykul z archiwum (status = 0).
     */
    public function restore(int $articleId): bool
    {
        $result = $this->db->update('pp_articles', ['status' => 0], ['id' => $articleId]);
        return (bool)$result;
    }

    /**
     * Trwale usuwa artykul wraz z relacjami i plikami z dysku.
     */
    public function deletePermanently(int $articleId): bool
    {
        $this->db->delete('pp_articles_pages', ['article_id' => $articleId]);
        $this->db->delete('pp_articles_langs', ['article_id' => $articleId]);
        $this->db->delete('pp_articles_images', ['article_id' => $articleId]);
        $this->db->delete('pp_articles_files', ['article_id' => $articleId]);
        $this->db->delete('pp_articles', ['id' => $articleId]);

        \Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/');
        \Shared\Helpers\Helpers::delete_dir('../upload/article_files/article_' . $articleId . '/');

        return true;
    }

    /**
     * Zwraca liste artykulow do panelu admin z filtrowaniem, sortowaniem i paginacja.
     *
     * @return array{items: array<int, array<string, mixed>>, total: int}
     */
    public function listForAdmin(
        array $filters,
        string $sortColumn = 'date_add',
        string $sortDir = 'DESC',
        int $page = 1,
        int $perPage = 15
    ): array {
        $sortColumn = trim($sortColumn);
        $sortDir = strtoupper(trim($sortDir));

        $allowedSortColumns = [
            'title' => 'title',
            'status' => 'pa.status',
            'date_add' => 'pa.date_add',
            'date_modify' => 'pa.date_modify',
            'user' => 'user',
        ];

        $sortSql = $allowedSortColumns[$sortColumn] ?? 'pa.date_add';
        $sortDir = $sortDir === 'ASC' ? 'ASC' : 'DESC';
        $page = max(1, $page);
        $perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
        $offset = ($page - 1) * $perPage;

        $where = ['pa.status != -1'];
        $params = [];

        $title = trim((string)($filters['title'] ?? ''));
        if (strlen($title) > 255) {
            $title = substr($title, 0, 255);
        }
        if ($title !== '') {
            $where[] = "(
                SELECT title
                FROM pp_articles_langs AS pal, pp_langs AS pl
                WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
                ORDER BY o ASC
                LIMIT 1
            ) LIKE :title";
            $params[':title'] = '%' . $title . '%';
        }

        if (($filters['status'] ?? '') !== '' && ($filters['status'] === '0' || $filters['status'] === '1')) {
            $where[] = 'pa.status = :status';
            $params[':status'] = (int)$filters['status'];
        }

        $this->appendDateRangeFilter($where, $params, 'pa.date_add', 'date_add_from', 'date_add_to', $filters);
        $this->appendDateRangeFilter($where, $params, 'pa.date_modify', 'date_modify_from', 'date_modify_to', $filters);

        $whereSql = implode(' AND ', $where);

        $sqlCount = "
            SELECT COUNT(0)
            FROM pp_articles AS pa
            WHERE {$whereSql}
        ";

        $stmtCount = $this->db->query($sqlCount, $params);
        $countRows = $stmtCount ? $stmtCount->fetchAll() : [];
        $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;

        $sql = "
            SELECT
                pa.id,
                pa.date_add,
                pa.date_modify,
                pa.status,
                (
                    SELECT title
                    FROM pp_articles_langs AS pal, pp_langs AS pl
                    WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
                    ORDER BY o ASC
                    LIMIT 1
                ) AS title,
                (
                    SELECT login
                    FROM pp_users AS pu
                    WHERE pu.id = pa.modify_by
                ) AS user
            FROM pp_articles AS pa
            WHERE {$whereSql}
            ORDER BY {$sortSql} {$sortDir}, pa.id {$sortDir}
            LIMIT {$perPage} OFFSET {$offset}
        ";

        $stmt = $this->db->query($sql, $params);
        $items = $stmt ? $stmt->fetchAll() : [];

        return [
            'items' => is_array($items) ? $items : [],
            'total' => $total,
        ];
    }

    /**
     * Zwraca liste artykulow z archiwum do panelu admin z filtrowaniem, sortowaniem i paginacja.
     *
     * @return array{items: array<int, array<string, mixed>>, total: int}
     */
    public function listArchivedForAdmin(
        array $filters,
        string $sortColumn = 'date_add',
        string $sortDir = 'DESC',
        int $page = 1,
        int $perPage = 15
    ): array {
        $sortColumn = trim($sortColumn);
        $sortDir = strtoupper(trim($sortDir));

        $allowedSortColumns = [
            'title' => 'title',
            'date_add' => 'pa.date_add',
            'date_modify' => 'pa.date_modify',
        ];

        $sortSql = $allowedSortColumns[$sortColumn] ?? 'pa.date_add';
        $sortDir = $sortDir === 'ASC' ? 'ASC' : 'DESC';
        $page = max(1, $page);
        $perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
        $offset = ($page - 1) * $perPage;

        $where = ['pa.status = -1'];
        $params = [];

        $title = trim((string)($filters['title'] ?? ''));
        if (strlen($title) > 255) {
            $title = substr($title, 0, 255);
        }
        if ($title !== '') {
            $where[] = "(
                SELECT title
                FROM pp_articles_langs AS pal, pp_langs AS pl
                WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
                ORDER BY o ASC
                LIMIT 1
            ) LIKE :title";
            $params[':title'] = '%' . $title . '%';
        }

        $this->appendDateRangeFilter($where, $params, 'pa.date_add', 'date_add_from', 'date_add_to', $filters);
        $this->appendDateRangeFilter($where, $params, 'pa.date_modify', 'date_modify_from', 'date_modify_to', $filters);

        $whereSql = implode(' AND ', $where);

        $sqlCount = "
            SELECT COUNT(0)
            FROM pp_articles AS pa
            WHERE {$whereSql}
        ";

        $stmtCount = $this->db->query($sqlCount, $params);
        $countRows = $stmtCount ? $stmtCount->fetchAll() : [];
        $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;

        $sql = "
            SELECT
                pa.id,
                pa.date_add,
                pa.date_modify,
                (
                    SELECT title
                    FROM pp_articles_langs AS pal, pp_langs AS pl
                    WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
                    ORDER BY o ASC
                    LIMIT 1
                ) AS title
            FROM pp_articles AS pa
            WHERE {$whereSql}
            ORDER BY {$sortSql} {$sortDir}, pa.id {$sortDir}
            LIMIT {$perPage} OFFSET {$offset}
        ";

        $stmt = $this->db->query($sql, $params);
        $items = $stmt ? $stmt->fetchAll() : [];

        return [
            'items' => is_array($items) ? $items : [],
            'total' => $total,
        ];
    }

    /**
     * Zapisuje kolejnosc zdjec galerii artykulu.
     */
    public function saveGalleryOrder(int $articleId, string $order): bool
    {
        $imageIds = explode(';', $order);
        if (!is_array($imageIds) || empty($imageIds)) {
            return true;
        }

        $position = 0;
        foreach ($imageIds as $imageId) {
            if ($imageId === '' || $imageId === null) {
                continue;
            }

            $this->db->update('pp_articles_images', [
                'o' => $position++,
            ], [
                'AND' => [
                    'article_id' => $articleId,
                    'id' => (int)$imageId,
                ],
            ]);
        }

        \Shared\Helpers\Helpers::delete_dir('../temp/');
        return true;
    }

    /**
     * Zapisuje kolejnosc zalacznikow artykulu.
     */
    public function saveFilesOrder(int $articleId, string $order): bool
    {
        $fileIds = explode(';', $order);
        if (!is_array($fileIds) || empty($fileIds)) {
            return true;
        }

        $position = 0;
        foreach ($fileIds as $fileId) {
            if ($fileId === '' || $fileId === null) {
                continue;
            }

            try {
                $this->db->update('pp_articles_files', [
                    'o' => $position++,
                ], [
                    'AND' => [
                        'article_id' => $articleId,
                        'id' => (int)$fileId,
                    ],
                ]);
            } catch (\Throwable $e) {
                // Fallback for instances where pp_articles_files does not yet have "o" column.
                return true;
            }
        }

        \Shared\Helpers\Helpers::delete_dir('../temp/');
        return true;
    }

    /**
     * Zwraca mape: article_id => etykieta stron (np. " - Strona A / Strona B").
     *
     * @param array<int, int> $articleIds
     * @return array<int, string>
     */
    public function pagesSummaryForArticles(array $articleIds): array
    {
        $normalizedIds = [];
        foreach ($articleIds as $articleId) {
            $id = (int)$articleId;
            if ($id > 0) {
                $normalizedIds[$id] = $id;
            }
        }

        if (empty($normalizedIds)) {
            return [];
        }

        $placeholders = [];
        $params = [];
        foreach (array_values($normalizedIds) as $index => $id) {
            $key = ':article_id_' . $index;
            $placeholders[] = $key;
            $params[$key] = $id;
        }

        $sql = "
            SELECT
                ap.article_id,
                ap.page_id,
                (
                    SELECT title
                    FROM pp_pages_langs AS ppl, pp_langs AS pl
                    WHERE ppl.lang_id = pl.id AND ppl.page_id = ap.page_id AND ppl.title != ''
                    ORDER BY pl.o ASC
                    LIMIT 1
                ) AS title
            FROM pp_articles_pages AS ap
            WHERE ap.article_id IN (" . implode(', ', $placeholders) . ")
            ORDER BY ap.article_id ASC, ap.o ASC, ap.page_id ASC
        ";

        $stmt = $this->db->query($sql, $params);
        $rows = $stmt ? $stmt->fetchAll() : [];
        if (!is_array($rows)) {
            return [];
        }

        $titlesByArticle = [];
        foreach ($rows as $row) {
            $articleId = (int)($row['article_id'] ?? 0);
            if ($articleId <= 0) {
                continue;
            }

            $title = trim((string)($row['title'] ?? ''));
            if ($title === '') {
                continue;
            }

            $titlesByArticle[$articleId][] = $title;
        }

        $summary = [];
        foreach (array_values($normalizedIds) as $articleId) {
            if (empty($titlesByArticle[$articleId])) {
                $summary[$articleId] = '';
                continue;
            }

            $summary[$articleId] = ' - ' . implode(' / ', $titlesByArticle[$articleId]);
        }

        return $summary;
    }

    public function updateImageAlt(int $imageId, string $imageAlt): bool
    {
        $result = $this->db->update('pp_articles_images', [
            'alt' => $imageAlt,
        ], [
            'id' => $imageId,
        ]);

        \Shared\Helpers\Helpers::delete_cache();

        return (bool)$result;
    }

    public function updateFileName(int $fileId, string $fileName): bool
    {
        $result = $this->db->update('pp_articles_files', [
            'name' => $fileName,
        ], [
            'id' => $fileId,
        ]);

        return (bool)$result;
    }

    public function markFileToDelete(int $fileId): bool
    {
        $result = $this->db->update('pp_articles_files', [
            'to_delete' => 1,
        ], [
            'id' => $fileId,
        ]);

        return (bool)$result;
    }

    public function markImageToDelete(int $imageId): bool
    {
        $result = $this->db->update('pp_articles_images', [
            'to_delete' => 1,
        ], [
            'id' => $imageId,
        ]);

        return (bool)$result;
    }

    private function isCheckedValue($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return ((int)$value) === 1;
        }

        if (is_string($value)) {
            $normalized = strtolower(trim($value));
            return in_array($normalized, ['1', 'on', 'true', 'yes'], true);
        }

        return false;
    }

    private function appendDateRangeFilter(
        array &$where,
        array &$params,
        string $column,
        string $fromKey,
        string $toKey,
        array $filters
    ): void {
        $from = trim((string)($filters[$fromKey] ?? ''));
        $to = trim((string)($filters[$toKey] ?? ''));

        if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
            $fromParam = ':' . str_replace('.', '_', $column) . '_from';
            $where[] = "{$column} >= {$fromParam}";
            $params[$fromParam] = $from . ' 00:00:00';
        }

        if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
            $toParam = ':' . str_replace('.', '_', $column) . '_to';
            $where[] = "{$column} <= {$toParam}";
            $params[$toParam] = $to . ' 23:59:59';
        }
    }

    /**
     * Usuwa nieprzypisane pliki artykulow (article_id = null) wraz z plikami z dysku.
     */
    public function deleteNonassignedFiles(): void
    {
        $results = $this->db->select('pp_articles_files', '*', ['article_id' => null]);
        if (is_array($results)) {
            foreach ($results as $row) {
                if (file_exists('../' . $row['src'])) {
                    unlink('../' . $row['src']);
                }
            }
        }

        $this->db->delete('pp_articles_files', ['article_id' => null]);
    }

    /**
     * Usuwa nieprzypisane zdjecia artykulow (article_id = null) wraz z plikami z dysku.
     */
    public function deleteNonassignedImages(): void
    {
        $results = $this->db->select('pp_articles_images', '*', ['article_id' => null]);
        if (is_array($results)) {
            foreach ($results as $row) {
                if (file_exists('../' . $row['src'])) {
                    unlink('../' . $row['src']);
                }
            }
        }

        $this->db->delete('pp_articles_images', ['article_id' => null]);
    }

    /**
     * Pobiera artykuly opublikowane w podanym zakresie dat.
     */
    public function articlesByDateAdd( string $dateStart, string $dateEnd, string $langId = 'pl' ): array
    {
        $stmt = $this->db->query(
            'SELECT id FROM pp_articles '
            . 'WHERE status = 1 '
            . 'AND date_add BETWEEN :date_start AND :date_end '
            . 'ORDER BY date_add DESC',
            [':date_start' => $dateStart, ':date_end' => $dateEnd]
        );

        $articles = [];
        $rows = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];

        if ( is_array( $rows ) ) {
            foreach ( $rows as $row ) {
                $articles[] = $this->articleDetailsFrontend( $row['id'], $langId );
            }
        }

        return $articles;
    }

    // =========================================================================
    // FRONTEND METHODS (z Redis cache)
    // =========================================================================

    /**
     * Pobiera szczegoly artykulu dla frontendu (z copy_from fallback + Redis cache).
     */
    public function articleDetailsFrontend(int $articleId, string $langId): ?array
    {
        $cacheHandler = new \Shared\Cache\CacheHandler();
        $cacheKey = "ArticleRepository::articleDetailsFrontend:{$articleId}:{$langId}";

        $objectData = $cacheHandler->get($cacheKey);

        if ($objectData) {
            return unserialize($objectData);
        }

        $article = $this->db->get('pp_articles', '*', ['id' => $articleId]);

        if (!$article) {
            return null;
        }

        $langRow = $this->db->get('pp_articles_langs', '*', [
            'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
        ]);

        if ($langRow) {
            if ($langRow['copy_from']) {
                $copyRow = $this->db->get('pp_articles_langs', '*', [
                    'AND' => ['article_id' => $articleId, 'lang_id' => $langRow['copy_from']]
                ]);
                $article['language'] = $copyRow ? $copyRow : $langRow;
            } else {
                $article['language'] = $langRow;
            }
        }

        $article['images'] = $this->db->select('pp_articles_images', '*', [
            'article_id' => $articleId,
            'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
        ]);

        $article['files'] = $this->db->select('pp_articles_files', '*', [
            'article_id' => $articleId
        ]);

        $article['pages'] = $this->db->select('pp_articles_pages', 'page_id', [
            'article_id' => $articleId
        ]);

        $cacheHandler->set($cacheKey, $article);

        return $article;
    }

    /**
     * Pobiera ID artykulow ze strony z sortowaniem i paginacja (z Redis cache).
     */
    public function articlesIds(int $pageId, string $langId, int $limit, int $sortType, int $from): ?array
    {
        $output = null;

        switch ($sortType) {
            case 0: $order = 'date_add ASC'; break;
            case 1: $order = 'date_add DESC'; break;
            case 2: $order = 'date_modify ASC'; break;
            case 3: $order = 'date_modify DESC'; break;
            case 4: $order = 'o ASC'; break;
            case 5: $order = 'title ASC'; break;
            case 6: $order = 'title DESC'; break;
            default: $order = 'id ASC'; break;
        }

        $cacheHandler = new \Shared\Cache\CacheHandler();
        $cacheKey = "ArticleRepository::articlesIds:{$pageId}:{$langId}:{$limit}:{$sortType}:{$from}:{$order}";

        $objectData = $cacheHandler->get($cacheKey);

        if ($objectData) {
            return unserialize($objectData);
        }

        $stmt = $this->db->query(
            'SELECT * FROM ( '
                . 'SELECT '
                    . 'a.id, date_modify, date_add, o, '
                    . '( CASE '
                        . 'WHEN copy_from IS NULL THEN title '
                        . 'WHEN copy_from IS NOT NULL THEN ( '
                            . 'SELECT title FROM pp_articles_langs '
                            . 'WHERE lang_id = al.copy_from AND article_id = a.id '
                        . ') '
                    . 'END ) AS title '
                . 'FROM '
                    . 'pp_articles_pages AS ap '
                    . 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
                    . 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
                . 'WHERE '
                    . 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
            . ') AS q1 '
            . 'WHERE q1.title IS NOT NULL '
            . 'ORDER BY q1.' . $order . ' '
            . 'LIMIT ' . (int)$from . ',' . (int)$limit,
            [':lang_id' => $langId]
        );
        $results = $stmt ? $stmt->fetchAll() : [];

        if (is_array($results) && !empty($results)) {
            foreach ($results as $row) {
                $output[] = $row['id'];
            }
        }

        $cacheHandler->set($cacheKey, $output);

        return $output;
    }

    /**
     * Zlicza artykuly na stronie (z Redis cache).
     */
    public function pageArticlesCount(int $pageId, string $langId): int
    {
        $cacheHandler = new \Shared\Cache\CacheHandler();
        $cacheKey = "ArticleRepository::pageArticlesCount:{$pageId}:{$langId}";

        $objectData = $cacheHandler->get($cacheKey);

        if ($objectData) {
            return (int)unserialize($objectData);
        }

        $stmt = $this->db->query(
            'SELECT COUNT(0) FROM ( '
                . 'SELECT '
                    . 'a.id, '
                    . '( CASE '
                        . 'WHEN copy_from IS NULL THEN title '
                        . 'WHEN copy_from IS NOT NULL THEN ( '
                            . 'SELECT title FROM pp_articles_langs '
                            . 'WHERE lang_id = al.copy_from AND article_id = a.id '
                        . ') '
                    . 'END ) AS title '
                . 'FROM '
                    . 'pp_articles_pages AS ap '
                    . 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
                    . 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
                . 'WHERE '
                    . 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
            . ') AS q1 '
            . 'WHERE q1.title IS NOT NULL',
            [':lang_id' => $langId]
        );
        $results = $stmt ? $stmt->fetchAll() : [];

        $count = isset($results[0][0]) ? (int)$results[0][0] : 0;

        $cacheHandler->set($cacheKey, $count);

        return $count;
    }

    /**
     * Pobiera paginowane artykuly ze strony.
     *
     * @return array{articles: ?array, ls: int}
     */
    public function pageArticles(array $page, string $langId, int $bs): array
    {
        $count = $this->pageArticlesCount((int)$page['id'], $langId);
        $articlesLimit = (int)($page['articles_limit'] ?: 10);
        $ls = (int)ceil($count / $articlesLimit);

        if ($bs < 1) {
            $bs = 1;
        } elseif ($bs > $ls) {
            $bs = $ls;
        }

        $from = $articlesLimit * ($bs - 1);

        if ($from < 0) {
            $from = 0;
        }

        return [
            'articles' => $this->articlesIds((int)$page['id'], $langId, $articlesLimit, (int)($page['sort_type'] ?? 0), $from),
            'ls' => $ls,
        ];
    }

    /**
     * Pobiera artykuly-aktualnosci ze strony (z sortowaniem wg page_sort).
     */
    public function news(int $pageId, int $limit, string $langId): ?array
    {
        $sort = (int)$this->db->get('pp_pages', 'sort_type', ['id' => $pageId]);

        $articlesIds = $this->articlesIds($pageId, $langId, $limit, $sort, 0);

        $articles = null;
        if (is_array($articlesIds) && !empty($articlesIds)) {
            foreach ($articlesIds as $articleId) {
                $articles[] = $this->articleDetailsFrontend($articleId, $langId);
            }
        }

        return $articles;
    }

    /**
     * Sprawdza czy artykul ma flage noindex (z Redis cache).
     */
    public function articleNoindex(int $articleId, string $langId): bool
    {
        $cacheHandler = new \Shared\Cache\CacheHandler();
        $cacheKey = "ArticleRepository::articleNoindex:{$articleId}:{$langId}";

        $objectData = $cacheHandler->get($cacheKey);

        if ($objectData) {
            return (bool)unserialize($objectData);
        }

        $noindex = $this->db->get('pp_articles_langs', 'noindex', [
            'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
        ]);

        $cacheHandler->set($cacheKey, $noindex);

        return (bool)$noindex;
    }

    /**
     * Pobiera najpopularniejsze artykuly ze strony (wg views DESC, z Redis cache).
     */
    public function topArticles(int $pageId, int $limit, string $langId): ?array
    {
        return $this->fetchArticlesByPage('topArticles', $pageId, $limit, $langId, 'views DESC');
    }

    /**
     * Pobiera najnowsze artykuly ze strony (wg date_add DESC, z Redis cache).
     */
    public function newsListArticles(int $pageId, int $limit, string $langId): ?array
    {
        return $this->fetchArticlesByPage('newsListArticles', $pageId, $limit, $langId, 'date_add DESC');
    }

    /**
     * Wspolna logika dla topArticles/newsListArticles (z Redis cache).
     */
    private function fetchArticlesByPage(string $cachePrefix, int $pageId, int $limit, string $langId, string $orderBy): ?array
    {
        $cacheHandler = new \Shared\Cache\CacheHandler();
        $cacheKey = "ArticleRepository::{$cachePrefix}:{$pageId}:{$limit}:{$langId}";

        $objectData = $cacheHandler->get($cacheKey);

        if (!$objectData) {
            $stmt = $this->db->query(
                'SELECT * FROM ( '
                    . 'SELECT '
                        . 'a.id, date_add, views, '
                        . '( CASE '
                            . 'WHEN copy_from IS NULL THEN title '
                            . 'WHEN copy_from IS NOT NULL THEN ( '
                                . 'SELECT title FROM pp_articles_langs '
                                . 'WHERE lang_id = al.copy_from AND article_id = a.id '
                            . ') '
                        . 'END ) AS title '
                    . 'FROM '
                        . 'pp_articles_pages AS ap '
                        . 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
                        . 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
                    . 'WHERE '
                        . 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
                . ') AS q1 '
                . 'WHERE q1.title IS NOT NULL '
                . 'ORDER BY q1.' . $orderBy . ' '
                . 'LIMIT 0, ' . (int)$limit,
                [':lang_id' => $langId]
            );
            $articlesData = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];

            $cacheHandler->set($cacheKey, $articlesData);
        } else {
            $articlesData = unserialize($objectData);
        }

        $articles = null;
        if (\Shared\Helpers\Helpers::is_array_fix($articlesData)) {
            foreach ($articlesData as $row) {
                $articles[] = $this->articleDetailsFrontend((int)$row['id'], $langId);
            }
        }

        return $articles;
    }
}
