<?php

namespace App\Http\Controllers;

use App\Agent\MarketingAgent;
use App\Agent\Workspace;
use App\Campaigns\Publisher;
use App\Campaigns\PublishGate;
use App\Models\Asset;
use App\Models\Campaign;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
use Laravel\Ai\Models\Conversation;
use Throwable;

/**
 * The chat.
 *
 * Thin on purpose: it moves a message into the agent and renders whatever came
 * back. Every decision about what the campaign becomes lives in a tool, and
 * every decision about whether it may be published lives in the gate.
 */
class ChatController extends Controller
{
    public function __construct(
        private readonly MarketingAgent $agent,
        private readonly Workspace $workspace,
        private readonly PublishGate $gate,
        private readonly Publisher $publisher,
    ) {}

    public function show(Request $request): View
    {
        $conversation = $this->resolveConversation($request);

        if ($conversation) {
            $this->workspace->bindTo($conversation->id);
        }

        return view('chat', [
            'conversationId' => $conversation?->id,
            'transcript' => $this->transcript($conversation),
            'conversations' => $this->recentConversations($request),
            ...$this->panel(),
        ]);
    }

    public function create(): RedirectResponse
    {
        return to_route('chat.show');
    }

    /** One message through the agent, with any files the user attached. */
    public function send(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'message' => ['nullable', 'string', 'max:4000'],
            'conversation' => ['nullable', 'string', 'max:36'],
            'files' => ['nullable', 'array', 'max:5'],
            'files.*' => ['file', 'mimes:jpg,jpeg,png,gif,mp4,mov', 'max:51200'],
        ]);

        $agent = $validated['conversation'] ?? null
            ? $this->agent->continue($validated['conversation'], $request->user())
            : $this->agent->forParticipant($request->user());

        // Attachments become assets before the turn runs, so the agent's very
        // first look at the workspace already sees them and does not ask the
        // user to upload what they just uploaded.
        $this->workspace->bindTo($agent->currentConversation());

        foreach ($request->file('files', []) as $file) {
            $this->workspace->produced($this->attach($file, $request->user()));
        }

        // Attaching focuses the asset; the campaign is what the user is talking
        // about, so put it back if there is one.
        if ($campaign = $this->latestCampaign()) {
            $this->workspace->focusOn($campaign);
        }

        try {
            $response = $agent->prompt($this->promptFor($validated, $request));
            $reply = trim((string) $response->text);
        } catch (Throwable $e) {
            report($e);

            // Never dead-end. Say what broke and confirm nothing changed.
            $reply = "That step failed: {$e->getMessage()}\n\nNothing in the campaign was changed.";
        }

        return response()->json([
            'conversation' => $agent->currentConversation(),
            'reply' => $reply !== '' ? $reply : $this->fallbackText(),
            ...$this->panel(),
        ]);
    }

    /**
     * Approve and carry out a publish.
     *
     * Separate from the chat on purpose: the agent can request publication, it
     * cannot perform it. This is the human act.
     */
    public function publish(Request $request): JsonResponse
    {
        $request->validate(['conversation' => ['required', 'string', 'max:36']]);

        $this->workspace->bindTo($request->string('conversation')->toString());

        if (! $campaign = $this->focusedCampaign()) {
            return response()->json(['ok' => false, 'error' => 'No campaign is in focus.', ...$this->panel()], 422);
        }

        $decision = $this->publisher->propose($campaign, 'web');

        if ($decision->isBlocked()) {
            return response()->json([
                'ok' => false,
                'error' => 'The campaign is not ready to publish.',
                'reasons' => $decision->evidence['blocking'] ?? [],
                ...$this->panel(),
            ], 422);
        }

        try {
            $decision = $this->publisher->approve($decision, 'web');
        } catch (Throwable $e) {
            return response()->json(['ok' => false, 'error' => $e->getMessage(), ...$this->panel()], 502);
        }

        return response()->json([
            'ok' => $decision->wasApplied(),
            'result' => $decision->result,
            'error' => $decision->reason,
            ...$this->panel(),
        ]);
    }

    // ----------------------------------------------------------------- shared

    /**
     * The right-hand panel, rendered server side so the campaign and the chat
     * can never disagree about what is stored.
     *
     * @return array<string,mixed>
     */
    private function panel(): array
    {
        $campaign = $this->focusedCampaign();

        return [
            'ready' => $campaign ? $this->gate->passes($campaign) : false,
            'published' => $campaign?->status === 'published',
            'panelHtml' => view('partials.campaign-panel', [
                'campaign' => $campaign,
                'blocking' => $campaign ? $this->gate->check($campaign) : [],
                'sources' => $campaign ? $this->workspace->sourcesFor($campaign) : [],
                'assets' => $this->workspace->isBound()
                    ? $this->workspace->artifacts(Asset::class)->map(fn ($l) => $l->artifactable)->filter()
                    : collect(),
            ])->render(),
        ];
    }

    private function focusedCampaign(): ?Campaign
    {
        if (! $this->workspace->isBound()) {
            return null;
        }

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

        return $focus instanceof Campaign ? $focus : $this->latestCampaign();
    }

    private function latestCampaign(): ?Campaign
    {
        return $this->workspace->isBound()
            ? $this->workspace->artifacts(Campaign::class)->first()?->artifactable
            : null;
    }

    private function attach(UploadedFile $file, $owner): Asset
    {
        return Asset::create([
            'owner_type' => $owner?->getMorphClass(),
            'owner_id' => $owner?->getKey(),
            'kind' => str_starts_with((string) $file->getMimeType(), 'video/') ? 'video' : 'image',
            'source' => 'upload',
            'path' => $file->store('assets'),
            'original_name' => $file->getClientOriginalName(),
            'mime' => $file->getMimeType(),
            'bytes' => $file->getSize(),
            'checksum' => hash_file('sha256', $file->getRealPath()),
        ]);
    }

    /** A message is optional when files are attached; say so rather than sending nothing. */
    private function promptFor(array $validated, Request $request): string
    {
        $message = trim((string) ($validated['message'] ?? ''));
        $count = count($request->file('files', []));

        if ($message !== '') {
            return $message;
        }

        return $count > 0
            ? "I have attached {$count} creative(s)."
            : 'Continue.';
    }

    /** Never return an empty bubble: say what is still missing instead. */
    private function fallbackText(): string
    {
        $campaign = $this->focusedCampaign();
        $blocking = $campaign ? $this->gate->check($campaign) : [];

        if (! $campaign) {
            return 'Tell me what you want to advertise and I will start a campaign.';
        }

        return $blocking === []
            ? 'The campaign is complete and ready to publish.'
            : 'Updated the campaign. Still needed: '.collect($blocking)->take(4)->implode('; ').'.';
    }

    private function resolveConversation(Request $request): ?Conversation
    {
        $id = $request->string('conversation')->toString();

        return $id !== ''
            ? Conversation::find($id)
            : $this->recentConversations($request)->first();
    }

    private function recentConversations(Request $request)
    {
        return Conversation::query()
            ->where('participant_type', $request->user()?->getMorphClass())
            ->where('participant_id', $request->user()?->getKey())
            ->latest('updated_at')
            ->limit(30)
            ->get();
    }

    /**
     * The user-facing transcript.
     *
     * Tool traffic is excluded: it is in the audit log, and replaying it in the
     * chat buries the sentence the user is actually meant to read.
     *
     * @return list<array{role: string, text: string}>
     */
    private function transcript(?Conversation $conversation): array
    {
        if (! $conversation) {
            return [];
        }

        return DB::table('agent_conversation_messages')
            ->where('conversation_id', $conversation->id)
            ->whereIn('role', ['user', 'assistant'])
            ->orderBy('created_at')
            ->orderBy('id')
            ->get(['role', 'content'])
            ->filter(fn ($m): bool => trim((string) $m->content) !== '')
            ->map(fn ($m): array => [
                'role' => $m->role === 'user' ? 'user' : 'agent',
                'text' => (string) $m->content,
            ])
            ->values()
            ->all();
    }
}
