<?php

namespace App\Models;

use App\Models\Concerns\Reportable;
use App\Tables\Changeset;
use App\Tables\Column;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use MightyPork\Exceptions\NotApplicableException;

/**
 * Change proposal
 *
 * @property int $id
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property int $table_id
 * @property int $revision_id
 * @property int $author_id
 * @property string $note
 * @property Changeset $changeset - JSONB
 * @property-read User $author
 * @property-read Table $table
 * @property-read Revision $revision
 */
class Proposal extends BaseModel
{
    use Reportable;
    protected $guarded = [];

    protected $touches = ['author', 'table'];

    /** Authoring user */
    public function author()
    {
        return $this->belongsTo(User::class, 'author_id');
    }

    /** Target revision */
    public function revision()
    {
        return $this->belongsTo(Revision::class);
    }

    /** Target table (that this was submitted to) */
    public function table()
    {
        return $this->belongsTo(Table::class);
    }

    public function scopeUnmerged(Builder $query, Table $table)
    {
        return $query->whereRaw('
            "id" NOT IN 
                (SELECT "proposal_id" FROM "revisions"
                    LEFT JOIN "table_revision_pivot"
                        ON "revisions"."id" = "table_revision_pivot"."revision_id"
                        WHERE "table_revision_pivot"."table_id" = ?)',
            [
                $table->getKey()
            ]);
    }

    public function getChangesetAttribute()
    {
        $changeset = Changeset::fromObject(fromJSON($this->attributes['changes'], true));
        $changeset->revision = $this->revision;
        $changeset->table = $this->getAttribute('table');
        $changeset->note = $this->note;

        return $changeset;
    }

    public function setChangesetAttribute($value)
    {
        if ($value instanceof Changeset) {
            $this->attributes['changes'] = toJSON($value->toObject());
        } else {
            throw new NotApplicableException("Only a Changeset may be set to Proposal->changes");
        }
    }

    /**
     * Create a new Proposal instance wrapping this changeset,
     * owned by the currently logged in User. The created instance
     * is NOT saved yet.
     *
     * @param Changeset $changeset - changeset to hydrate the proposal with
     * @return Proposal
     */
    public static function fromChangeset(Changeset $changeset)
    {
        if (!$changeset->hasAnyChanges()) {
            throw new NotApplicableException('No changes to propose.');
        }

        if (empty($changeset->note)) {
            throw new NotApplicableException('Proposal note must be filled.');
        }

        if ($changeset->table == null || !$changeset->table instanceof Table) {
            throw new NotApplicableException('Table not assigned to Changeset');
        }

        if ($changeset->revision == null || !$changeset->revision instanceof Revision) {
            throw new NotApplicableException('Revision not assigned to Changeset');
        }

        // Assign unique row IDs to new rows (they use temporary negative IDs in a draft)
        $changeset->renumberRows();

        return new Proposal([
            // relations
            'table_id' => $changeset->table->getKey(),
            'revision_id' => $changeset->revision->getKey(),
            'author_id' => \user()->getKey(),
            // the proposal info
            'note' => $changeset->note,
            'changeset' => $changeset, // this is without a note
        ]);
    }

    /**
     * Accept the proposed changes: create a new revision based on the parent revision
     * and changes described in this Proposal, and link it to the table.
     */
    public function createRevision()
    {
        DB::transaction(function () {
            $changeset = $this->changeset;

            $columns = $changeset->fetchAndTransformColumns();

            $newRevision = new Revision([
                'ancestor_id' => $changeset->revision->getKey(),
                'proposal_id' => $this->getKey(),
                'note' => $changeset->note,
                'row_count' => 0, // Will be set later when we are sure about the row count
                'columns' => array_map(function(Column $c) {
                    return $c->toArray(false);
                }, $columns),
            ]);

            $newRevision->save(); // this gives it an ID, needed to associate rows

            // --- Copy over rows that are left unchanged ---
            // ...this directly works with the pivot
            $removedRowIds = (array)($changeset->removedRows ?? []);
            $changedRowIds = array_keys((array)($changeset->rowUpdates ?? []));
            $excludedGRIDs = array_merge($removedRowIds, $changedRowIds);

            $excluded_ids = [];

            if ($excludedGRIDs) {
                $questionmarks = str_repeat('?,', count($excludedGRIDs) - 1) . '?';
                $excluded_ids = $changeset->revision->rows()->whereRaw("data->'_id' IN ($questionmarks)", $excludedGRIDs)
                    ->get(['id'])->pluck('id')->toArray();
            }

            $query = '
              INSERT INTO revision_row_pivot 
                SELECT ? as revision_id, row_id FROM revision_row_pivot 
                  WHERE revision_id = ?';
            $subs = [
                $newRevision->getKey(),
                $changeset->revision->getKey()
            ];
            if ($excluded_ids) {
                $questionmarks = str_repeat('?,', count($excluded_ids) - 1) . '?';
                $query .= ' AND row_id NOT IN (' . $questionmarks . ')';
                $subs = array_merge($subs, $excluded_ids);
            }

            DB::statement($query, $subs);

            // --- Insert modified rows ---
            if ($changeset->rowUpdates) {
                $ids = array_keys($changeset->rowUpdates);
                $questionmarks = str_repeat('?,', count($ids) - 1) . '?';

                $toChange = $changeset->revision->rows()->whereRaw("data->'_id' IN ($questionmarks)", $ids)->get();

                $updateData = [];
                foreach ($toChange as $row) {
                    $updateData[] = new Row([
                        'data' => $changeset->transformRow($row->data, false),
                    ]);
                }
                $newRevision->rows()->saveMany($updateData);
            }

            // --- Insert new rows ---
            if ($changeset->newRows) {
                $newRowData = [];
                foreach ($changeset->newRows as $newRow) {
                    $newRowData[] = new Row(['data' => $newRow]);
                }
                $newRevision->rows()->saveMany($newRowData);
            }

            $newRevision->update(['row_count' => $newRevision->rows()->count()]);

            // --- Attach this revision to the table ---
            $changeset->table->revisions()->save($newRevision);
            $changeset->table->update(['revision_id' => $newRevision->getKey()]);
        });
    }
}