<?php

namespace Lucent\Record;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService;
use Lucent\Edge\Edge;
use Lucent\Edge\EdgeService;
use Lucent\File\FileService;
use Lucent\Id\Id;
use Lucent\LucentException;
use Lucent\Query\Query;
use Lucent\Record\InputData\EdgeInputData;
use Lucent\Record\InputData\RecordInputData;
use Lucent\Revision\RevisionService;
use Lucent\Schema\FieldInterface;
use Lucent\Schema\Schema;
use Lucent\Schema\Type;
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,
        private FileService     $fileService,
    )
    {
    }

    /**
     * @param string $url
     * @param RecordInputData $data
     * @param list<EdgeInputData> $edges
     * @return string
     * @throws LucentException
     * @throws ValidatorException
     */
    public function createFromUrl(
        string          $url,
        RecordInputData $data,
        array           $edges
    ): string
    {
        $schema = $this->channelService->getSchema($data->schemaName)->get();
        if ($schema->type !== Type::FILES) {
            throw new LucentException("You can't upload a file to a regular record");
        }
        $fileData = $this->fileService->createFromUrl($schema, $url);
        if ($fileData->isDuplicate) {
            return $fileData->duplicateId;
        }
        return $this->create($data, $fileData->recordFile, $edges);
    }

    public function createFromUploadedFile(
        UploadedFile    $uploadedFile,
        RecordInputData $data,
        array           $edges
    ): string
    {
        $schema = $this->channelService->getSchema($data->schemaName)->get();
        if ($schema->type !== Type::FILES) {
            throw new LucentException("You can't upload a file to a regular record");
        }
        $fileData = $this->fileService->upload($schema, $uploadedFile);
        if ($fileData->isDuplicate) {
            return $fileData->duplicateId;
        }
        return $this->create($data, $fileData->recordFile, $edges);
    }

    public function createFromFileData(
        FileData        $fileData,
        RecordInputData $data,
        array           $edges
    ): string
    {
        $schema = $this->channelService->getSchema($data->schemaName)->get();
        if ($schema->type !== Type::FILES) {
            throw new LucentException("You can't upload a file to a regular record");
        }
        return $this->create($data, $fileData, $edges);
    }

    /**
     * @param RecordInputData $data
     * @param FileData|null $file
     * @param list<EdgeInputData> $edges
     * @return string
     * @throws ValidatorException
     */
    public function create(
        RecordInputData $data,
        ?FileData       $file = null,
        array           $edges = []
    ): string
    {

        $formattedData = $this->inputFormatter->fill($data->schemaName, new RecordData($data->data));
        $newRecordId = empty($data->id) ? Id::new() : $data->id;

        $record = new Record(
            id: $newRecordId,
            schema: $data->schemaName,
            status: $data->status,
            _sys: System::newRecord($this->authService->currentUserId()),
            data: $formattedData,
            _file: !empty($file) ? FileData::fromArray(toArray($file)) : null,
        );

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

        RecordRepo::create($record);
        $newEdges = $this->edgeService->createManyForRecord($record->id, $record->schema, $edges);
        $this->revisionService->create($record, $newEdges);
        return $record->id;

    }

    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public function update(
        string $id,
        array  $data,
        Status $status,
    ): 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));

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

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

        RecordRepo::update($newRecord);
        $newEdges = $this->edgeService->findForSource($record->id);
        $this->revisionService->create($newRecord, $newEdges);

    }

    /**
     * @throws LucentException
     * @throws ValidatorException
     */
    public function updateWithEdges(
        string $id,
        array  $data,
        Status $status,
        array  $edges
    ): 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));

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

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

        RecordRepo::update($newRecord);
        $newEdges = $this->edgeService->replaceManyForRecord($record->id, $record->schema, $edges);
        $this->revisionService->create($newRecord, $newEdges);

    }

    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(fn(Edge $edge) => new EdgeInputData(
                target: $edge->target,
                targetSchema: $edge->targetSchema,
                field: $edge->field,
            ))->toArray();

        $record->id = $newRecordId;

        return $this->create(
            new RecordInputData(
                schemaName: $record->schema,
                id: $record->id,
                data: $record->data->toArray(),
                status: Status::DRAFT
            ),
            file: $record->_file,
            edges: $newEdgesData
        );

    }


    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->updateWithEdges(
            id: $revision->recordId,
            data: $revision->data->toArray(),
            status: Status::DRAFT,
            edges: array_map(fn(Edge $edge) => new EdgeInputData($edge->target, $edge->targetSchema, $edge->field), $revision->_edges),
        );
    }


    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,
        );

    }
}
