<?php
namespace boru\ai\Conversation;

use boru\ai\Asset\Attachment;
use boru\ai\Asset\ProviderAssetBinding;
use boru\ai\Persistence\PersistenceProviderInterface;
use boru\ai\Schema\Message;
use boru\ai\Schema\Request;
use boru\ai\Schema\Response;
use boru\ai\Schema\ToolCall;
use boru\ai\Schema\ToolResult;

class ConversationForkService
{
    public function fork(PersistenceProviderInterface $persistence, $sourceReference, $targetReference, array $options = array())
    {
        $sourceReference = $this->normalizeReference($sourceReference);
        $targetReference = $this->normalizeReference($targetReference);

        $source = $persistence->loadConversation($sourceReference);
        if (!$source instanceof Conversation) {
            throw new \RuntimeException('Source conversation not found');
        }

        $target = $this->copyConversation($source, $targetReference, $options);
        $persistence->saveConversation($target);
        foreach ($target->turns() as $turn) {
            $persistence->saveTurn($target, $turn);
        }

        return $target;
    }

    public function cloneConversation(PersistenceProviderInterface $persistence, $sourceReference, $targetReference, array $options = array())
    {
        $options['through_turn'] = null;
        return $this->fork($persistence, $sourceReference, $targetReference, $options);
    }

    public function copyConversation(Conversation $source, ConversationReference $targetReference, array $options = array())
    {
        $targetReference = $this->copyReferenceWithoutId($targetReference);

        $throughTurn = $this->resolveThroughTurn($source, $options);
        $target = new Conversation($targetReference);
        $target->provider($source->provider());
        $target->model($source->model());

        if ($this->optionBool($options, 'copy_metadata', true)) {
            $target->metadata($this->copyArray($source->metadata()));
        }

        $target->metadata('forked_from', array(
            'conversation_id' => $source->reference()->id(),
            'external_reference' => $source->reference()->externalReference(),
            'user_id' => $source->reference()->userId(),
            'through_turn' => $throughTurn,
            'forked_at' => date('c'),
        ));

        if ($this->optionBool($options, 'copy_memory', true)) {
            $target->memory($this->copyMemory($source->memory(), $options));
        }

        $target->turns($this->copyTurns($source->turns(), $throughTurn, $options));
        return $target;
    }

    public function normalizeReference($reference)
    {
        if ($reference instanceof ConversationReference) {
            return $reference;
        }

        $normalized = new ConversationReference();
        $normalized->externalReference((string)$reference);
        return $normalized;
    }

    protected function copyReferenceWithoutId(ConversationReference $reference)
    {
        $copy = new ConversationReference();
        $copy->externalReference($reference->externalReference());
        $copy->userId($reference->userId());
        $copy->metadata($this->copyArray($reference->metadata()));
        return $copy;
    }

    protected function copyMemory(ConversationMemory $memory, array $options)
    {
        $copy = new ConversationMemory();
        $copy->title($memory->title());
        $copy->summary($memory->summary());
        $copy->metadata($this->copyArray($memory->metadata()));

        if ($this->optionBool($options, 'copy_notes', true)) {
            $notes = array();
            foreach ($memory->notes() as $note) {
                if ($note instanceof ConversationNote) {
                    $notes[] = $this->copyNote($note);
                }
            }
            $copy->notes($notes);
        }

        return $copy;
    }

    protected function copyNote(ConversationNote $note)
    {
        $copy = new ConversationNote();
        $copy->kind($note->kind());
        $copy->text($note->text());
        $copy->confidence($note->confidence());
        $copy->sourceTurnNumber($note->sourceTurnNumber());
        $copy->metadata($this->copyArray($note->metadata()));
        $copy->createdAt($note->createdAt());
        $copy->updatedAt($note->updatedAt());
        return $copy;
    }

    protected function copyTurns(array $turns, $throughTurn, array $options)
    {
        $copies = array();
        foreach ($turns as $turn) {
            if (!$turn instanceof Turn) {
                continue;
            }
            if ($throughTurn !== null && $turn->number() > $throughTurn) {
                continue;
            }
            $copies[] = $this->copyTurn($turn, $options);
        }
        return $copies;
    }

    protected function copyTurn(Turn $turn, array $options)
    {
        $copy = new Turn();
        $copy->number($turn->number());
        $copy->status($turn->status());
        $copy->metadata($this->copyArray($turn->metadata()));
        $copy->startedAt($turn->startedAt());
        $copy->completedAt($turn->completedAt());

        if ($turn->request() instanceof Request) {
            $copy->request($this->copyRequest($turn->request(), $options));
        }
        if ($turn->response() instanceof Response) {
            $copy->response($this->copyResponse($turn->response(), $options));
        }

        $toolCalls = array();
        foreach ($turn->toolCalls() as $toolCall) {
            if ($toolCall instanceof ToolCall) {
                $toolCalls[] = $this->copyToolCall($toolCall);
            }
        }
        $copy->toolCalls($toolCalls);

        $toolResults = array();
        foreach ($turn->toolResults() as $toolResult) {
            if ($toolResult instanceof ToolResult) {
                $toolResults[] = $this->copyToolResult($toolResult);
            }
        }
        $copy->toolResults($toolResults);

        return $copy;
    }

    protected function copyRequest(Request $request, array $options)
    {
        $copy = new Request();
        $copy->provider($request->provider());
        $copy->model($request->model());
        $copy->instructions($request->instructions());
        $copy->messages($this->copyMessages($request->messages()));
        $copy->tools($this->copyArray($request->tools()));
        $copy->toolResults($this->copyToolResults($request->toolResults()));
        $copy->attachments($this->copyAttachments($request->attachments()));
        $copy->assetBindings($this->copyAssetBindings($request->assetBindings()));
        $copy->options($this->copyArray($request->options()));
        $copy->metadata($this->copyArray($request->metadata()));

        if ($this->providerContinuity($options) === 'scrub') {
            $requestOptions = $copy->options();
            unset($requestOptions['previous_response_id']);
            $copy->options($requestOptions);
        }
        return $copy;
    }

    protected function copyMessages(array $messages)
    {
        $copies = array();
        foreach ($messages as $message) {
            if ($message instanceof Message) {
                $copies[] = new Message($message->role(), $message->toArray()['parts'], $this->copyArray($message->metadata()));
            }
        }
        return $copies;
    }

    protected function copyToolResults(array $toolResults)
    {
        $copies = array();
        foreach ($toolResults as $toolResult) {
            if ($toolResult instanceof ToolResult) {
                $copies[] = $this->copyToolResult($toolResult);
            }
        }
        return $copies;
    }

    protected function copyAttachments(array $attachments)
    {
        $copies = array();
        foreach ($attachments as $attachment) {
            if (!$attachment instanceof Attachment) {
                continue;
            }
            $copy = new Attachment();
            $copy->id($attachment->id());
            $copy->assetId($attachment->assetId());
            $copy->kind($attachment->kind());
            $copy->detail($attachment->detail());
            $copy->metadata($this->copyArray($attachment->metadata()));
            $copies[] = $copy;
        }
        return $copies;
    }

    protected function copyAssetBindings(array $bindings)
    {
        $copies = array();
        foreach ($bindings as $binding) {
            if (!$binding instanceof ProviderAssetBinding) {
                continue;
            }
            $copy = new ProviderAssetBinding();
            $copy->id($binding->id());
            $copy->assetId($binding->assetId());
            $copy->provider($binding->provider());
            $copy->providerAssetId($binding->providerAssetId());
            $copy->providerUri($binding->providerUri());
            $copy->purpose($binding->purpose());
            $copy->status($binding->status());
            $copy->mimeType($binding->mimeType());
            $copy->metadata($this->copyArray($binding->metadata()));
            $copy->raw($this->copyValue($binding->raw()));
            $copy->createdAt($binding->createdAt());
            $copy->updatedAt($binding->updatedAt());
            $copies[] = $copy;
        }
        return $copies;
    }

    protected function copyResponse(Response $response, array $options)
    {
        $copy = new Response();
        if ($this->providerContinuity($options) !== 'scrub') {
            $copy->id($response->id());
            $copy->raw($this->copyValue($response->raw()));
        }
        $copy->provider($response->provider());
        $copy->model($response->model());
        $copy->status($response->status());
        $copy->text($response->text());
        $copy->usage($this->copyArray($response->usage()));
        $copy->error($this->copyValue($response->error()));
        $copy->metadata($this->copyArray($response->metadata()));

        $toolCalls = array();
        foreach ($response->toolCalls() as $toolCall) {
            if ($toolCall instanceof ToolCall) {
                $toolCalls[] = $this->copyToolCall($toolCall);
            }
        }
        $copy->toolCalls($toolCalls);

        return $copy;
    }

    protected function copyToolCall(ToolCall $toolCall)
    {
        $copy = new ToolCall();
        $copy->id($toolCall->id());
        $copy->name($toolCall->name());
        $copy->arguments($this->copyArray($toolCall->arguments()));
        $copy->argumentsJson($toolCall->argumentsJson());
        $copy->status($toolCall->status());
        $copy->metadata($this->copyArray($toolCall->metadata()));
        return $copy;
    }

    protected function copyToolResult(ToolResult $toolResult)
    {
        $copy = new ToolResult();
        $copy->toolCallId($toolResult->toolCallId());
        $copy->name($toolResult->name());
        $copy->output($this->copyValue($toolResult->output()));
        $copy->success($toolResult->success());
        $copy->metadata($this->copyArray($toolResult->metadata()));
        return $copy;
    }

    protected function resolveThroughTurn(Conversation $source, array $options)
    {
        if (array_key_exists('through_turn', $options) && $options['through_turn'] !== null) {
            return (int)$options['through_turn'];
        }

        $latest = $source->latestTurn();
        return $latest instanceof Turn ? $latest->number() : null;
    }

    protected function providerContinuity(array $options)
    {
        $value = isset($options['provider_continuity']) ? strtolower((string)$options['provider_continuity']) : 'preserve';
        return $value === 'scrub' ? 'scrub' : 'preserve';
    }

    protected function optionBool(array $options, $key, $default)
    {
        if (!array_key_exists($key, $options)) {
            return $default ? true : false;
        }
        return $options[$key] ? true : false;
    }

    protected function copyArray(array $value)
    {
        return $this->copyValue($value);
    }

    protected function copyValue($value)
    {
        if (is_array($value)) {
            $copy = array();
            foreach ($value as $key => $item) {
                $copy[$key] = $this->copyValue($item);
            }
            return $copy;
        }
        if (is_object($value)) {
            return clone $value;
        }
        return $value;
    }
}
