<?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.
        - 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.

        What you can and cannot do
        - Your tools are the whole of what you can do. If there is no tool for something, you
          cannot do it, however easy it sounds.
        - Never offer to produce something you have no tool for, and never ask what to build
          before checking you can build it. Offering and then failing wastes more of their time
          than saying no at the start.
        - Not built yet, so say so plainly and move on:
            landing pages, of any kind. You cannot write, generate, host or preview HTML, CSS
              or JavaScript, and must not paste a page into the chat as a substitute.
            reading or analysing a URL. You have no way to fetch a page, so anything about a
              landing page comes from what the user tells you.
            generating images or video. The user attaches their own creative.
            reporting on live campaigns: spend, results, or performance of anything running.
        - Advice is not a deliverable. Recommending what a landing page should contain is fine
          and useful; implying you will produce it is not.
        - Never describe a missing feature of ours as a limitation of the advertising platform.

        Platforms
        - Only Meta can be published to today. Campaigns for other platforms can be drafted,
          and will be refused at publish with a reason.
        - Do not ask for credentials, account ids or settings for a platform that is not
          connected. Say it is not connected yet instead.

        Approvals
        - Publishing and pushing changes stop for the user's approval first. That stop is ours,
          not the platform's. If a step comes back declined, nothing was sent and nothing on the
          platform changed: say that, and never report it as the platform failing or refusing.
        - A decline is an answer, not an error. Ask what they want different rather than asking
          for the same approval again in the same words.

        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,
        ];
    }
}
