<?php

namespace App\Services\Publishing;

use App\Enums\PlatformEnum;
use App\Exceptions\PublishException;
use App\Models\Article;
use App\Models\ArticlePublication;
use App\Models\PlatformChannel;
use App\Models\PlatformChannelPost;
use App\Models\RouteArticle;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Log\LogSaver;
use Exception;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
use RuntimeException;

class ArticlePublishingService
{
    private const LOCK_TTL_SECONDS = 180;

    private const LOCK_WAIT_SECONDS = 15;

    public function __construct(private LogSaver $logSaver) {}

    /**
     * Factory seam to create publisher instances (helps testing without network calls)
     */
    protected function makePublisher(mixed $account): LemmyPublisher
    {
        return new LemmyPublisher($account);
    }

    /**
     * Publish an article to the channel specified by a route_article record.
     *
     * @param  array<string, mixed>  $extractedData
     *
     * @throws PublishException
     */
    public function publishRouteArticle(RouteArticle $routeArticle, array $extractedData): PublishOutcome
    {
        $article = $routeArticle->article;
        $channel = $routeArticle->platformChannel;

        if (! $channel) {
            throw new PublishException($article, PlatformEnum::LEMMY, new RuntimeException('ROUTE_ARTICLE_MISSING_CHANNEL'));
        }

        if (! $channel->relationLoaded('platformInstance')) {
            $channel->load(['platformInstance', 'activePlatformAccounts']);
        }

        $account = $channel->activePlatformAccounts()->first();

        if (! $account) {
            $this->logSaver->warning('No active account for channel', $channel, [
                'article_id' => $article->id,
                'route_article_id' => $routeArticle->id,
            ]);

            return PublishOutcome::failure('No active account for channel');
        }

        return $this->publishToChannel($article, $extractedData, $channel, $account);
    }

    /**
     * @param  array<string, mixed>  $extractedData
     */
    private function publishToChannel(Article $article, array $extractedData, PlatformChannel $channel, mixed $account): PublishOutcome
    {
        $lock = Cache::lock("publish:{$article->id}:{$channel->id}", self::LOCK_TTL_SECONDS);

        try {
            return $lock->block(self::LOCK_WAIT_SECONDS, function () use ($article, $extractedData, $channel, $account) {
                $alreadyPublished = ArticlePublication::where('article_id', $article->id)
                    ->where('platform_channel_id', $channel->id)
                    ->exists();

                if ($alreadyPublished) {
                    $this->logSaver->info('Skipping duplicate: already published to channel', $channel, [
                        'article_id' => $article->id,
                    ]);

                    return PublishOutcome::skipped('Already published to this channel');
                }

                return $this->doPublishToChannel($article, $extractedData, $channel, $account);
            });
        } catch (LockTimeoutException $e) {
            $this->logSaver->info('Skipping publish: another worker holds the lock', $channel, [
                'article_id' => $article->id,
            ]);

            return PublishOutcome::skipped('Another worker is publishing this article');
        }
    }

    /**
     * @param  array<string, mixed>  $extractedData
     */
    private function doPublishToChannel(Article $article, array $extractedData, PlatformChannel $channel, mixed $account): PublishOutcome
    {
        try {
            // Check if this URL or title was already posted to this channel
            $title = $extractedData['title'] ?? $article->title;
            if (PlatformChannelPost::duplicateExists($channel, $article->url, $title)) {
                $this->logSaver->info('Skipping duplicate: URL or title already posted to channel', $channel, [
                    'article_id' => $article->id,
                    'url' => $article->url,
                    'title' => $title,
                ]);

                return PublishOutcome::skipped('URL or title already posted to this channel');
            }

            $publisher = $this->makePublisher($account);
            $postData = $publisher->publishToChannel($article, $extractedData, $channel);

            $publication = ArticlePublication::create([
                'article_id' => $article->id,
                'post_id' => $postData['post_view']['post']['id'],
                'platform_channel_id' => $channel->id,
                'published_by' => $account->username,
                'published_at' => now(),
                'platform' => $channel->platformInstance->platform->value,
                'publication_data' => $postData,
            ]);

            $this->logSaver->info('Published to channel', $channel, [
                'article_id' => $article->id,
            ]);

            return PublishOutcome::published($publication);
        } catch (Exception $e) {
            $this->logSaver->warning('Failed to publish to channel', $channel, [
                'article_id' => $article->id,
                'error' => $e->getMessage(),
            ]);

            return PublishOutcome::failure($e->getMessage());
        }
    }
}
