<?php

namespace App\Agent;

use App\Models\ArtifactFieldSource;
use App\Models\ConversationArtifact;
use Illuminate\Database\Eloquent\Model;

/**
 * The system prompt, rebuilt from the database on every turn.
 *
 * This is the only route by which current state reaches the model. The
 * transcript carries the conversation; the workspace carries the truth. It
 * works because laravel/ai calls instructions() on each generation rather than
 * capturing it once.
 *
 * Static guidance comes first so it stays cacheable across turns, live state last.
 */
class Instructions
{
    public function __construct(private readonly Workspace $workspace) {}

    public function build(): string
    {
        return implode("\n\n", array_filter([
            $this->rules(),
            $this->whatExists(),
            $this->currentFocus(),
            $this->needsConfirming(),
        ]));
    }

    private function rules(): string
    {
        return <<<'TEXT'
        You are a media buying assistant. You work by calling tools and then handing the
        result to the user for approval. You are talking to someone who buys media for a
        living, not to a customer.

        Working style
        - Fill in everything you can from what the user already said before asking for anything.
        - Ask for at most two things at once, and only for what you genuinely cannot work out.
        - Keep replies under 90 words. No preamble, no repeating what the user just said.

        Honesty
        - Never say a value is set unless a tool call in this turn confirmed it. Do not write
          "I have verified" or "I have selected" for something you did not just do.
        - Never invent an ad account, Page, pixel, image hash or campaign id. Fetch them and
          pass back what the platform returned, exactly.
        - Never ask again for something the state below shows is already done.
        - If there is no tool for what the user wants, say so plainly. Never describe a missing
          feature of ours as a limitation of the advertising platform.
        - When a tool fails, say what failed and what you need. No vague apologies.
        - Never call the same tool twice with the same arguments after it failed.

        One conversation, many things
        - A conversation is a workspace, not a single campaign. It may hold several campaigns,
          landing pages and creatives at once.
        - Work on whatever is in focus below. If the user clearly means something else, switch
          focus first, then act.
        - Creating something focuses it automatically. Do not switch focus straight after
          creating something.

        Changes
        - Anything with a value is already set. Do not set it again unprompted.
        - An explicit instruction overrides that. If the user names a different budget, country
          or account, change it even over an existing value.
        - If the user says a value is wrong, do not re-set it to the same thing. Fetch the
          options, show them, and ask which they meant.
        TEXT;
    }

    /** What this conversation has produced, so the model stops re-creating things. */
    private function whatExists(): string
    {
        $artifacts = $this->workspace->artifacts();

        if ($artifacts->isEmpty()) {
            return "IN THIS CONVERSATION\nNothing has been created yet.";
        }

        $lines = $artifacts
            ->map(fn (ConversationArtifact $link): string => sprintf(
                '  - %s #%s%s',
                class_basename($link->artifactable_type),
                $link->artifactable_id,
                $this->label($link->artifactable),
            ))
            ->implode("\n");

        return "IN THIS CONVERSATION\n".$lines;
    }

    private function currentFocus(): string
    {
        $focus = $this->workspace->focus();

        if (! $focus) {
            return "CURRENTLY IN FOCUS\nNothing. If the user refers to something above, switch focus to it first.";
        }

        return "CURRENTLY IN FOCUS\n".class_basename($focus).' #'.$focus->getKey()."\n"
            .json_encode($this->summarise($focus), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Values the model guessed after something failed. These can never reach a
     * platform without the user confirming them, so the model is told to ask.
     */
    private function needsConfirming(): ?string
    {
        $focus = $this->workspace->focus();

        if (! $focus) {
            return null;
        }

        $unconfirmed = $this->workspace->unconfirmed($focus);

        if ($unconfirmed === []) {
            return null;
        }

        return "GUESSED, NOT YET CONFIRMED\n"
            .'These were guessed after a lookup failed and cannot be used until the user confirms them: '
            .implode(', ', $unconfirmed).'.';
    }

    /** @return array<string,mixed> */
    private function summarise(Model $focus): array
    {
        $attributes = collect($focus->attributesToArray())
            ->except(['created_at', 'updated_at'])
            ->filter(fn (mixed $value): bool => $value !== null && $value !== [])
            ->all();

        return [
            ...$attributes,
            'where_each_value_came_from' => $this->workspace->sourcesFor($focus) ?: 'nothing recorded yet',
        ];
    }

    private function label(?Model $artifact): string
    {
        return $artifact?->getAttribute('name') ? ' "'.$artifact->getAttribute('name').'"' : '';
    }

    /** Kept so callers can reason about provenance without importing the model. */
    public function sources(): array
    {
        return [
            ArtifactFieldSource::USER_STATED,
            ArtifactFieldSource::PLATFORM_RETURNED,
            ArtifactFieldSource::MODEL_INFERRED,
            ArtifactFieldSource::FALLBACK,
        ];
    }
}
