<?php

namespace App\Http\Controllers;

use App\Models\Revision;
use App\Models\Table;
use App\Models\User;
use App\Tables\Changeset;
use App\Tables\Column;
use App\Tables\ColumnNumerator;
use App\Tables\CStructArrayExporter;
use App\Tables\CsvExporter;
use App\Tables\CXMacroExporter;
use App\Tables\JsonExporter;
use App\Tables\PhpExporter;
use App\Tables\RowNumerator;
use DB;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use MightyPork\Exceptions\NotApplicableException;
use MightyPork\Exceptions\SimpleValidationException;
use MightyPork\Utils\Utils;

class TableController extends Controller
{
    private function resolveTable(Request $request, User $user, string $table)
    {
        /** @var Table $tableModel */
        $tableModel = $user->tables()->where('name', $table)->first();
        if ($tableModel === null) abort(404, "No such table.");
        return $tableModel;
    }

    public function revertTo(Request $request, User $user, string $table)
    {
        $input = $this->validate($request, [
            'rev' => 'int'
        ]);

        $tableModel = $this->resolveTable($request, $user, $table);
        $this->authorize('edit', $tableModel);

        $revisionNum = (int)$input->rev;
        $revision = $tableModel->revisions()->orderBy('created_at')->skip($revisionNum - 1)->first();
        if ($revision === null) abort(404, "No such revision");

        $tableModel->revision()->associate($revision);
        $tableModel->save();

        return redirect($tableModel->revisionsRoute);
    }

    public function view(Request $request, User $user, string $table)
    {
        $input = $this->validate($request, [
            'rev' => 'nullable|int'
        ]);

        /** @var Table $tableModel */
        $tableModel = $user->tables()->withCount(['favourites', 'forks', 'revisions', 'comments'])
            ->where('name', $table)->first();
        if ($tableModel === null) abort(404, "No such table.");

        // option to show other revisions
        $revisionNum = $tableModel->revisions_count;
        if ($input->has('rev')) {
            $revisionNum = (int)$input->rev;
            $revision = $tableModel->revisions()->orderBy('created_at')->skip($revisionNum - 1)->first();
            if ($revision === null) abort(404, "No such revision");
        } else {
            $revision = $tableModel->revision;
        }

        $this->countTableVisit($request, $tableModel);

        $columns = Column::columnsFromJson($revision->columns);

        $rq = $revision->rowsData($columns);
        if (count($columns)) $rq = $rq->sortByCol($columns[0]);
        $rows = $rq->paginate(25, []);

        return view('table.view', [
            'table' => $tableModel,
            'revision' => $revision,
            'revisionNum' => $revisionNum,
            'proposals_count' => $tableModel->proposals()->unmerged($tableModel)->count(),
            'columns' => $columns,
            'rows' => $rows,
        ]);
    }

    public function delete(Request $request, User $user, string $table)
    {
        $tableModel = $this->resolveTable($request, $user, $table);
        $this->authorize('delete', $tableModel);

        if ($request->get('table-name', null) !== $table) {
            return $this->backWithErrors(['table-name' => 'Fill table name']);
        }

        $tableModel->delete();

        return redirect(route('profile.view', $user->name));
    }

    /**
     * SHow a form for creating a new table
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        $exampleData = "";

        $columns = Column::columnsFromJson([
            ['id' => 1, 'name' => 'column_1', 'type' => 'string', 'title' => 'First Column'],
        ]);

        return view('table.create', [
            'exampleColumns' => '',
            'columns' => $columns,
            'exampleData' => $exampleData,
        ]);
    }

    /**
     * Show table settings edit form
     */
    public function settings(Request $request, User $user, string $table)
    {
        $tableModel = $this->resolveTable($request, $user, $table);
        $this->authorize('edit', $tableModel);

        return view('table.conf', [
            'table' => $tableModel,
        ]);
    }

    /**
     * Store modified table settings
     */
    public function storeSettings(Request $request, User $user, string $table)
    {
        $tableModel = $this->resolveTable($request, $user, $table);
        $this->authorize('edit', $tableModel);

        $input = $this->validate($request, [
            'name' => [
                'required',
                VALI_NAME,
            ],
            'title' => ['required', VALI_LINE],
            'description' => ['nullable', VALI_TEXT],
            'license' => ['nullable', VALI_TEXT],
            'origin' => ['nullable', VALI_TEXT],
        ]);

        $otherTableNames = $user->tables()->get(['name'])->pluck('name')->diff([$table])->all();
        if (in_array($input->name, $otherTableNames)) {
            return $this->backWithErrors(['name' => "You already have a table called \"$input->name\""]);
        }

        $tableModel->fill($input->all());
        $tableModel->save();

        flash()->success('Table settings saved');

        return redirect($tableModel->viewRoute); // the route now changed
    }

    public function viewRevisions(Request $request, User $user, string $table)
    {
        $tableModel = $this->resolveTable($request, $user, $table);

        $revisions = $tableModel->revisions()->orderBy('created_at', 'desc')->get();

        return view('table.revisions', [
            'table' => $tableModel,
            'revisions' => $revisions,
        ]);
    }

    public function storeNew(Request $request)
    {
        /** @var User $u */
        $u = \Auth::user();

        $input = $this->validate($request, [
            'name' => [
                'required',
                VALI_NAME,
            ],
            'title' => ['required', VALI_LINE],
            'description' => ['nullable', VALI_TEXT],
            'license' => ['nullable', VALI_TEXT],
            'origin' => ['nullable', VALI_TEXT],
            'columns' => 'required|json',
            'data' => 'string|nullable',
        ]);

        // Check if table name is unique for user
        if ($u->hasTable($input->name)) {
            return $this->backWithErrors([
                'name' => "You already have a table called \"$input->name\"",
            ]);
        }

        // --- COLUMNS ---
        // Parse and validate the columns specification
        /** @var Column[] $columns */
        $columns = [];
        $col_names = []; // for checking duplicates
        $colsArray = fromJSON($input->columns);

        // prevent griefing via long list of columns
        if (count($colsArray) > 100) return $this->backWithErrors(['columns' => "Too many columns"]);

        foreach ($colsArray as $colObj) {
            // ensure column has a title
            if (!isset($colObj->title)) {
                $colObj->title = $colObj->name;
            }

            try {
                if (in_array($colObj->name, $col_names)) {
                    return $this->backWithErrors(['columns' => "Duplicate column: $colObj->name"]);
                }

                $col_names[] = $colObj->name;
                $columns[] = new Column($colObj);
            } catch (\Exception $e) {
                // validation errors from the Column constructor
                return $this->backWithErrors(['columns' => $e->getMessage()]);
            }
        }
        if (count($columns) == 0) {
            return $this->backWithErrors(['columns' => "Define at least one column"]);
        }

        // Now assign column IDs
        $columnNumerator = new ColumnNumerator(count($columns));
        foreach ($columns as $column) {
            $column->setID($columnNumerator->next());
        }

        // --- DATA ---
        $fname = 'csv-file';
        $dataCsvLines = [];
        if ($request->hasFile($fname)) {
            try {
                $file = $request->file($fname);
                if ($file->isValid() && $file->isReadable()) {
                    $handle = $file->openFile();
                    $dataCsvLines = Utils::csvToArray($handle);
                    if (empty($dataCsvLines)) throw new \Exception("Failed to parse CSV file.");
                    $handle = null;
                } else {
                    throw new \Exception("File not valid.");
                }
            } catch (\Exception $e) {
                return $this->backWithErrors(['csv-file' => $e->getMessage()]);
            }
        }
        else if ($input->data) {
            try {
                $text = trim($input->data);
                if (!empty($text)) {
                    $dataCsvLines = Utils::csvToArray($text);
                }
            } catch (\Exception $e) {
                return $this->backWithErrors(['data' => $e->getMessage()]);
            }
        }

        // Preparing data to insert into the Rows table
        $rowsToInsert = null;
        $rowNumerator = null;
        try {
            $rowsToInsert = (new Changeset)->csvToRowsArray($columns, $dataCsvLines, false)->all();
        } catch (\Exception $e) {
            return $this->backWithErrors(['data' => $e->getMessage()]);
        }

        // --- STORE TO DB ---

        /* Fields for the new Revision instance */
        $revisionFields = [
            'note' => "Initial revision of table $u->name/$input->name",
            'columns' => $columns,
            'row_count' => count($rowsToInsert),
        ];

        /* Fields for the new Table instance */
        $tableFields = [
            'owner_id' => $u->id,
            'name' => $input->name,
            'title' => $input->title,
            'description' => $input->description,
            'license' => $input->license,
            'origin' => $input->origin,
        ];

        /** @var Table $table */
        $table = null;
        \DB::transaction(function () use ($revisionFields, $tableFields, $rowsToInsert, &$table) {
            $newRevision = Revision::create($revisionFields);

            $tableFields['revision_id'] = $newRevision->id; // current revision (not-null constraint on this FK)
            $table = Table::create($tableFields);

            // Attach the revision to the table, set as current
            $table->revisions()->attach($newRevision);
            $table->revision()->associate($newRevision);

            // Spawn rows, linked to the revision
            if (!empty($rowsToInsert)) {
                // this replicates the code in Proposal->toRevision
                $conn = DB::connection();
                $prepared = $conn->getPdo()->prepare('INSERT INTO rows (data) VALUES (?)');
                foreach (array_chunk($rowsToInsert, 10000) as $i => $chunk) {
                    $ids = [];
                    foreach ($chunk as $newRow) {
                        $prepared->execute([json_encode($newRow)]);
                        $ids[] = $conn->getPdo()->lastInsertId();
                    }

                    $qms = rtrim(str_repeat('('.$newRevision->id.', ?),', count($ids)), ',');
                    $conn->statement('INSERT INTO revision_row_pivot (revision_id, row_id) VALUES '.$qms.';',
                        $ids);
                }
            }
        });

        return redirect($table->viewRoute);
    }

    /**
     * Check unique visit, filter bots / scripts, and increment visits count.
     *
     * @param Request $request
     * @param Table $table
     */
    private function countTableVisit(Request $request, Table $table)
    {
        $cookieName = "view_{$table->owner->name}_{$table->name}";
        if (!$request->cookie($cookieName, false)) {
            $ua = $request->userAgent();
            // Filter out suspicious user agents
            if (! str_contains(strtolower($ua), Controller::BOT_USER_AGENTS)) {
                $table->countVisit();
                \Cookie::queue($cookieName, true, 24*60); // in minutes
            }
        }
    }

    /**
     * Simple export via a preset
     *
     * @param Request $request
     * @param User $user
     * @param string $table
     */
    public function export(Request $request, User $user, string $table)
    {
        $tableModel = $this->resolveTable($request, $user, $table);

        $exporter = null;

        switch ($request->get('format')) {
            case 'json':
                $exporter = new JsonExporter($tableModel);
                break;

            case 'csv':
                $exporter = new CsvExporter($tableModel);
                break;

            case 'csv-tab':
                $exporter = (new CsvExporter($tableModel))->withDelimiter("\t");
                break;

            case 'c':
                $exporter = new CStructArrayExporter($tableModel);
                break;

            case 'xmacro':
                $noq = explode(',', $request->input('noq',''));
                $exporter = (new CXMacroExporter($tableModel))->noQuotesAround($noq);
                break;

            case 'php':
                $exporter = new PhpExporter($tableModel);
                break;

            case 'js':
                $exporter = (new JsonExporter($tableModel))->withJsWrapper();
                break;

            default:
                abort(400, "Unspecified or unknown format.");
        }

        $dl = Utils::parseBool($request->get('dl', false));
        $exporter->exportToBrowser($dl);
    }
}