<?php

declare(strict_types=1);

namespace App\Http\Controllers\Admin;

use App\Classes\Validator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;

class BackupController extends AdminController
{
    public string $date;

    /**
     * Конструктор
     */
    public function __construct()
    {
        if (function_exists('set_time_limit')) {
            set_time_limit(600);
        }

        $this->date = date('d-M-Y_H-i-s', SITETIME);
    }

    /**
     * Главная страница
     */
    public function index(): View
    {
        $files = glob(storage_path('backups/*.{zip,gz,bz2,sql}'), GLOB_BRACE);
        arsort($files);

        return view('admin/backups/index', compact('files'));
    }

    /**
     * Создание нового бэкапа
     */
    public function create(Request $request, Validator $validator): View|RedirectResponse
    {
        if ($request->isMethod('post')) {
            $sheets = check($request->input('sheets'));
            $method = $request->input('method');
            $level = int($request->input('level'));

            $validator->equal($request->input('_token'), csrf_token(), __('validator.token'))
                ->notEmpty($sheets, ['sheets' => __('admin.backup.no_tables_save')])
                ->in($method, ['none', 'gzip', 'bzip'], ['method' => __('admin.backup.wrong_compression_method')])
                ->between($level, 0, 9, ['level' => __('admin.backup.wrong_compression_ratio')]);

            if ($validator->isValid()) {
                $selectTables = DB::select('SHOW TABLE STATUS where name IN("' . implode('","', $sheets) . '")');

                $limit = 3000;
                $filename = 'backup_' . $this->date . '.sql';

                $fp = $this->fopen(storage_path('backups/' . $filename), 'w', $method, $level);

                foreach ($selectTables as $table) {
                    $show = DB::selectOne("SHOW CREATE TABLE `{$table->Name}`");
                    $columnsFields = DB::select("SHOW COLUMNS FROM `{$table->Name}`");
                    $columns = '(' . implode(',', array_column($columnsFields, 'Field')) . ')';

                    $this->fwrite($fp, "--\n-- Structure table `{$table->Name}`\n--\n\n", $method);
                    $this->fwrite($fp, "DROP TABLE IF EXISTS `{$table->Name}`;\n{$show->{'Create Table'}};\n\n", $method);

                    $total = DB::table($table->Name)->count();

                    if (! $total) {
                        continue;
                    }

                    $this->fwrite($fp, "--\n-- Dump table `{$table->Name}`\n--\n\n", $method);
                    $this->fwrite($fp, "INSERT INTO `{$table->Name}` {$columns} VALUES ", $method);

                    for ($i = 0; $i < $total; $i += $limit) {
                        $cols = DB::table($table->Name)->lockForUpdate()->limit($limit)->offset($i)->get();

                        foreach ($cols as $key => $col) {
                            $records = get_object_vars($col);
                            $columns = [];

                            foreach ($records as $record) {
                                $record = is_int($record) || is_null($record) ? $record : "'" . str_replace("'", "''", $record) . "'";
                                $columns[] = $record ?? 'null';
                            }

                            $this->fwrite($fp, ($key || $i ? ',' : '') . '(' . implode(',', $columns) . ')', $method);
                            unset($columns);
                        }
                        unset($cols);
                    }

                    $this->fwrite($fp, ";\n\n", $method);
                }

                $this->fclose($fp, $method);

                setFlash('success', __('admin.backup.database_success_saved'));

                return redirect('admin/backups');
            }

            setInput($request->all());
            setFlash('danger', $validator->getErrors());
        }

        $tables = DB::select('SHOW TABLE STATUS');

        $bzopen = function_exists('bzopen');
        $gzopen = function_exists('gzopen');

        $levels = range(0, 9);

        return view('admin/backups/create', compact('tables', 'bzopen', 'gzopen', 'levels'));
    }

    /**
     * Удаляет сохраненный бэкап
     */
    public function delete(Request $request, Validator $validator): RedirectResponse
    {
        $file = $request->input('file');

        $validator->equal($request->input('_token'), csrf_token(), __('validator.token'))
            ->notEmpty($file, __('admin.backup.backup_not_indicated'))
            ->regex($file, '|^[\w\.\-]+$|i', __('admin.backup.invalid_backup_name'))
            ->true(file_exists(storage_path('backups/' . $file)), __('admin.backup.backup_not_exist'));

        if ($validator->isValid()) {
            unlink(storage_path('backups/' . $file));

            setFlash('success', __('admin.backup.backup_success_deleted'));
        } else {
            setFlash('danger', $validator->getErrors());
        }

        return redirect('admin/backups');
    }

    /**
     * Открывает поток
     *
     * @return bool|resource
     */
    private function fopen(string $name, string $mode, string $method, int $level)
    {
        if ($method === 'bzip') {
            return bzopen($name . '.bz2', $mode);
        }

        if ($method === 'gzip') {
            return gzopen($name . '.gz', "{$mode}b{$level}");
        }

        return fopen($name, $mode . 'b');
    }

    /**
     * Записывает данные в поток
     *
     * @param resource $fp
     */
    private function fwrite($fp, string $str, string $method): void
    {
        if ($method === 'bzip') {
            bzwrite($fp, $str);
        } elseif ($method === 'gzip') {
            gzwrite($fp, $str);
        } else {
            fwrite($fp, $str);
        }
    }

    /**
     * Закрывает поток
     *
     * @param resource $fp
     */
    private function fclose($fp, string $method): void
    {
        if ($method === 'bzip') {
            bzclose($fp);
        } elseif ($method === 'gzip') {
            gzclose($fp);
        } else {
            fflush($fp);
            fclose($fp);
        }
    }
}
