<?php

namespace Lucent\Record;

use Illuminate\Support\Str;
use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService;
use Lucent\Edge\Edge;
use Lucent\Edge\EdgeCollection;
use Lucent\Edge\EdgeService;
use Lucent\File\FileService;
use Lucent\Id\Id;
use Lucent\LucentException;
use Lucent\Query\Query;
use Lucent\Revision\RevisionService;
use Lucent\Schema\FieldInterface;
use Lucent\Schema\Schema;
use Lucent\Schema\Validator\Validator;
use Lucent\Schema\Validator\ValidatorException;

readonly class RecordService
{

    public function __construct(
        private AuthService     $authService,
        private RevisionService $revisionService,
        private ChannelService  $channelService,
        private Validator       $recordValidator,
        private Query           $query,
        private InputFormatter  $inputFormatter,
        private RecordRepo      $recordRepo,
        private EdgeService     $edgeService
    )
    {
    }

    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public
    function create(
        string  $schemaName,
        array   $data,
        ?string $id = null,
        array   $file = [],
        array   $edges = [],
        string  $status = "draft",
        string  $uploadFromUrl = "",
    ): string
    {
        $schema = $this->channelService->getSchema($schemaName)->get();

        $formattedData = $this->inputFormatter->fill($schemaName, new RecordData($data));
        if (empty($formattedData["id"])) {
            $formattedData["id"] = Id::new();
        }
        $uploadResult = FileService::create($schema, $uploadFromUrl, $file);

        $newRecordId = empty($id) ? Id::new() : $id;
        $uniqueEdges = collect($edges)
            ->map(function ($edge, $index) use ($newRecordId, $schemaName) {
                $edge['source'] = $newRecordId;
                $edge['sourceSchema'] = $schemaName;
                $edge["rank"] = $index;
                return (array)(new Edge(...$edge));
            })
            ->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
            ->values()->toArray();
        $uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
        if ($uploadResult->isDuplicate) {

            $this->edgeService->update($uploadResult->duplicateId, $uniqueEdgesCollection);
            return $uploadResult->duplicateId;
        }

        $record = new Record(
            id: $newRecordId,
            schema: $schema->name,
            status: Status::from($status),
            _sys: System::newRecord($this->authService->currentUserId()),
            data: $formattedData,
            _file: $uploadResult->recordFile,
        );

        if (Status::from($status) === Status::PUBLISHED) {
            $errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
            if ($errors->isNotEmpty()) {
                $this->recordValidator->throwException($errors);
            }
        }

        RecordRepo::create($record);
        $this->edgeService->update($record->id, $uniqueEdgesCollection);
        $this->revisionService->create($record, $uniqueEdgesCollection);
        return $record->id;

    }


    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public function update(
        string $id,
        array  $data,
        string $status = "draft",
        array  $edges = [],
        bool   $updateEdges = false
    ): void
    {
        $record = $this->query->filter(["id" => $id])->run()->records->first();

        if (empty($record)) {
            throw new LucentException("Record id is missing");
        }
        $formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));

        $uniqueEdgesCollection = new EdgeCollection();
        if ($updateEdges) {
            $uniqueEdges = collect($edges)
                ->map(function ($edge, $index) {
                    $edge["rank"] = $index;
                    $edgeData = (array)(new Edge(...$edge));
                    return $edgeData;
                })
                ->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
                ->values()->toArray();
            $uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
        }

        if (Status::from($status) === Status::PUBLISHED) {
            $errors = $this->recordValidator->check($record->schema, $formattedData, $uniqueEdgesCollection);

            if ($errors->isNotEmpty()) {
                $this->recordValidator->throwException($errors);
            }

            if ($errors->isNotEmpty()) {
                $this->recordValidator->throwException($errors);
            }
        }


        $newRecord = new Record(
            id: $record->id,
            schema: $record->schema,
            status: Status::from($status),
            _sys: $record->_sys->update($this->authService->currentUserId()),
            data: $record->data->merge($formattedData),
            _file: $record->_file,
        );

        RecordRepo::update($newRecord);
        if ($updateEdges) {
            $this->edgeService->update($newRecord->id, $uniqueEdgesCollection);
        }

        $this->revisionService->create($newRecord, $uniqueEdgesCollection);

    }

    /**
     */
    public function changeStatusBulk(
        string $status,
        array  $recordsIds,
    ): void
    {
        RecordRepo::updateStatusBulk(Status::from($status), $recordsIds);
    }

    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public
    function clone(
        string $recordId,
    ): string
    {
        $graph = $this->query
            ->filter(["id" => $recordId])
            ->limit(1)
            ->childrenDepth(1)
            ->run();

        $record = $graph->records->first();
        if (empty($record)) {
            throw new LucentException("Record id is missing");
        }

        $newRecordId = (string)Str::uuid();
        $newEdgesData = $graph->edges
            ->filter(fn(Edge $edge) => $edge->source == $recordId)
            ->values()
            ->map(function (Edge $edge) use ($newRecordId) {
                $edge->source = $newRecordId;
                return $edge->toArray();
            })->toArray();

        $record->id = $newRecordId;

        return $this->create(
            schemaName: $record->schema,
            data: $record->data->toArray(),
            id: $record->id,
            file: $record->_file?->toArray() ?? [],
            edges: $newEdgesData,
            status: "draft"
        );

    }


    public function deleteMany(
        array $recordsIds,
    ): void
    {
        $this->recordRepo->deleteMany($recordsIds);
    }

    public function emptyTrash(
        string $schemaName,
    ): void
    {

        $schema = $this->channelService->getSchema($schemaName)->get();
        $this->recordRepo->deleteTrashedBySchema($schemaName);
    }


    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public function rollback(
        string $recordId,
        int    $version,
    ): void
    {
        $revision = $this->revisionService->getByRecordIdAndVersion($recordId, $version)->get();
        $this->update(
            id: $revision->recordId,
            data: $revision->data->toArray(),
            edges: toArray($revision->_edges),
            updateEdges: true
        );
    }


    public function createEmpty(
        Schema $schema,
    ): Record
    {

        $defaultValues = $schema->fields->reduce(function ($carry, FieldInterface $f) {
            $carry[$f->name] = $f->default ?? null;
            return $carry;
        }, []);

        $formattedData = $this->inputFormatter->fill($schema->name, new RecordData($defaultValues));

        return new Record(
            id: Id::new(),
            schema: $schema->name,
            status: Status::DRAFT,
            _sys: System::newRecord($this->authService->currentUserId()),
            data: $formattedData,
            _file: null,
        );

    }
}
