<?php

namespace App\Services;

use App\Models\Page;
use App\Models\PageVersionHistory;
use App\Models\TranslatedPage;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class PageTranslationSyncService
{
    public function __construct(
        private readonly FranchiseGlobalSourceResolver $franchiseGlobalSourceResolver,
    ) {
    }

    public const STATE_NOT_ADOPTED = 'not_adopted';
    public const STATE_UP_TO_DATE = 'up_to_date';
    public const STATE_OUTDATED = 'outdated';
    public const STATE_LOCALLY_MODIFIED = 'locally_modified';

    private const TRANSLATABLE_TRANSLATED_PAGE_FIELDS = [
        'translated_content',
        'status',
        'is_global_suggestion',
        'seo_title',
        'seo_description',
    ];

    /**
     * Is this save staging unpublished edits rather than publishing them?
     *
     * A missing status means "publish" so that existing callers which never passed one
     * keep their previous behaviour.
     *
     * @param  array<string, mixed>  $attributes
     */
    private function isDraftSave(array $attributes): bool
    {
        if (!array_key_exists('status', $attributes)) {
            return false;
        }

        return !$this->isPublished((string) ($attributes['status'] ?? ''));
    }

    /**
     * Stage unpublished edits on the row WITHOUT touching the published copy.
     *
     * translated_content / status / version are deliberately left alone: the live site keeps
     * serving the last published content while a draft is outstanding.
     *
     * @param  array<string, mixed>  $attributes
     */
    private function stageDraftOnTranslation(TranslatedPage $translation, array $attributes): void
    {
        $translation->draft_content = $attributes['translated_content'] ?? [];
        $translation->draft_seo_title = $attributes['seo_title'] ?? null;
        $translation->draft_seo_description = $attributes['seo_description'] ?? null;
        $translation->draft_updated_at = Carbon::now();
        $translation->draft_updated_by = $this->currentUserId();
    }

    /**
     * Auth::id() resolves the session guard, which throws when there is no request bound —
     * i.e. from a console command or a queued job. Attribution is nice-to-have; never let it
     * take down a save.
     */
    private function currentUserId(): ?int
    {
        try {
            return Auth::id();
        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * Drop the working copy — used on publish (the draft has been promoted) and whenever the
     * row is overwritten from global (reset / adopt), where a surviving draft would be stale.
     */
    private function clearDraftOnTranslation(TranslatedPage $translation): void
    {
        $translation->draft_content = null;
        $translation->draft_seo_title = null;
        $translation->draft_seo_description = null;
        $translation->draft_updated_at = null;
        $translation->draft_updated_by = null;
    }

    /**
     * Seed a brand-new row that has never been published: no published copy exists yet.
     */
    private function initialiseUnpublishedTranslation(TranslatedPage $translation): void
    {
        $translation->translated_content = null;
        $translation->status = 'draft';
        $translation->version = 1;
    }

    public function resolveGlobalRootPage(Page $page): Page
    {
        if ($page->parent_page_id) {
            $parent = Page::query()->find($page->parent_page_id);

            return $parent ?: $page;
        }

        if ($page->is_global || $page->franchisee_id === null) {
            return $page;
        }

        return $page;
    }

    public function findFranchiseOverridePage(string $globalPageId, string $franchiseeId): ?Page
    {
        return Page::query()
            ->where('franchisee_id', $franchiseeId)
            ->where('parent_page_id', $globalPageId)
            ->first();
    }

    /**
     * Page id used for franchise TranslatedPage rows (override page when present, else global page).
     */
    public function resolveTranslatedPageStorageId(string $globalPageId, string $franchiseeId): string
    {
        $override = $this->findFranchiseOverridePage($globalPageId, $franchiseeId);

        return $override?->id ?? $globalPageId;
    }

    public function ensureFranchiseOverridePage(Page $globalPage, string $franchiseeId): Page
    {
        if (!$globalPage->is_global || $globalPage->franchisee_id !== null) {
            return $globalPage;
        }

        return DB::transaction(function () use ($globalPage, $franchiseeId) {
            $existing = $this->findFranchiseOverridePage($globalPage->id, $franchiseeId);
            if ($existing) {
                return $existing;
            }

            $franchisePage = Page::create([
                'franchisee_id' => $franchiseeId,
                'is_global' => false,
                'parent_page_id' => $globalPage->id,
                'title' => $globalPage->title,
                'slug' => $globalPage->slug,
                'template_id' => $globalPage->template_id,
                'content' => $globalPage->content,
                'status' => $globalPage->status,
                'seo_title' => $globalPage->seo_title,
                'seo_description' => $globalPage->seo_description,
                'is_required_for_translation' => $globalPage->is_required_for_translation,
                'page_type' => $globalPage->page_type,
            ]);

            TranslatedPage::query()
                ->where('page_id', $globalPage->id)
                ->where('franchisee_id', $franchiseeId)
                ->update(['page_id' => $franchisePage->id]);

            $globalTranslations = TranslatedPage::query()
                ->where('page_id', $globalPage->id)
                ->whereNull('franchisee_id')
                ->whereNull('deleted_at')
                ->get();

            foreach ($globalTranslations as $globalTranslation) {
                $exists = TranslatedPage::query()
                    ->where('page_id', $franchisePage->id)
                    ->where('franchisee_id', $franchiseeId)
                    ->where('language_code', strtolower((string) $globalTranslation->language_code))
                    ->exists();

                if ($exists) {
                    continue;
                }

                TranslatedPage::create([
                    'franchisee_id' => $franchiseeId,
                    'page_id' => $franchisePage->id,
                    'language_code' => strtolower((string) $globalTranslation->language_code),
                    'translated_content' => $globalTranslation->translated_content,
                    'status' => $globalTranslation->status,
                    'seo_title' => $globalTranslation->seo_title,
                    'seo_description' => $globalTranslation->seo_description,
                    'is_global_suggestion' => false,
                    'version' => 1,
                    'adopted_from_global' => true,
                    'source_global_page_id' => $globalPage->id,
                    'source_language_code' => strtolower((string) $globalTranslation->language_code),
                    'source_global_translation_id' => $globalTranslation->id,
                    'source_global_version' => max(1, (int) ($globalTranslation->version ?? 1)),
                    'last_local_edit_at' => null,
                ]);
            }

            return $franchisePage->fresh() ?? $franchisePage;
        });
    }

    public function resolveGlobalTranslatedPage(Page $globalPage, string $language): ?TranslatedPage
    {
        $language = strtolower($language);

        $row = TranslatedPage::query()
            ->where('page_id', $globalPage->id)
            ->whereNull('franchisee_id')
            ->where('language_code', $language)
            ->first();

        if ($row) {
            return $row;
        }

        if ($language === 'en') {
            return $this->virtualEnglishGlobalTranslation($globalPage);
        }

        return null;
    }

    /**
     * Global row used for version compare / diffs: tracked FK when present, else exact language, else English global.
     */
    public function resolveGlobalTranslationForVersionCompare(
        Page $globalPage,
        ?TranslatedPage $franchiseTranslation,
        string $languageCode,
    ): ?TranslatedPage {
        $languageCode = strtolower($languageCode);

        if ($franchiseTranslation && $franchiseTranslation->source_global_translation_id) {
            $tracked = TranslatedPage::query()
                ->whereNull('franchisee_id')
                ->where('id', $franchiseTranslation->source_global_translation_id)
                ->first();
            if ($tracked) {
                return $tracked;
            }
        }

        $exact = TranslatedPage::query()
            ->where('page_id', $globalPage->id)
            ->whereNull('franchisee_id')
            ->where('language_code', $languageCode)
            ->first();

        if ($exact) {
            return $exact;
        }

        if ($languageCode !== 'en') {
            return $this->resolveGlobalTranslatedPage($globalPage, 'en');
        }

        return $this->resolveGlobalTranslatedPage($globalPage, 'en');
    }

    public function franchiseRowEligibleForGlobalVersionTracking(TranslatedPage $franchiseTranslation): bool
    {
        if (strtolower((string) $franchiseTranslation->language_code) === 'en') {
            return true;
        }

        if ($franchiseTranslation->source_global_translation_id) {
            return true;
        }

        if (
            $franchiseTranslation->source_global_page_id
            && $franchiseTranslation->source_language_code
            && $franchiseTranslation->source_global_version !== null
        ) {
            return true;
        }

        return (bool) $franchiseTranslation->adopted_from_global;
    }

    public function hasGlobalPageUpdateAvailable(
        Page $globalPage,
        ?TranslatedPage $franchiseTranslation,
        string $languageCode,
    ): bool {
        return $this->hasFranchisePageUpdateAvailable(
            $globalPage,
            $franchiseTranslation,
            strtolower($languageCode),
        );
    }

    /**
     * Global content offered for franchise preferred language: global preferred row if it has
     * renderable content, else English mirror / virtual English from the global page.
     */
    public function getCurrentGlobalSourceTranslation(Page $globalPage, string $preferredLanguage, bool $publishedOnly = false): ?TranslatedPage
    {
        $pref = strtolower(trim($preferredLanguage));
        if ($pref === 'english') {
            $pref = 'en';
        }
        $effective = $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($pref);

        if ($effective !== 'en') {
            $glPref = $this->resolveGlobalTranslatedPage($globalPage, $effective);
            $c = is_array($glPref?->translated_content) ? $glPref->translated_content : [];
            if ($glPref && $this->translationHasRenderableContent($c)) {
                if ($publishedOnly && !$this->isPublished($glPref->status)) {
                    return null;
                }

                return $glPref;
            }
        }

        $glEn = $this->resolveGlobalTranslatedPage($globalPage, 'en');
        if ($glEn) {
            if ($publishedOnly && !$this->isPublished($glEn->status)) {
                return null;
            }

            return $glEn;
        }

        $virtual = $this->virtualEnglishGlobalTranslation($globalPage);
        if ($publishedOnly && !$this->isPublished($virtual->status)) {
            return null;
        }

        return $virtual;
    }

    public function franchiseStoredSourceKey(TranslatedPage $franchiseTranslation, Page $globalPage): string
    {
        $tid = $franchiseTranslation->source_global_translation_id;
        if ($tid) {
            $lang = strtolower((string) ($franchiseTranslation->source_language_code ?: $franchiseTranslation->language_code));

            return $lang . ':t:' . $tid;
        }

        $lang = strtolower((string) ($franchiseTranslation->source_language_code ?: 'en'));
        $pid = (string) ($franchiseTranslation->source_global_page_id ?? $globalPage->id);

        return $lang . ':page:' . $pid;
    }

    public function globalSourceKeyForTranslation(Page $globalPage, ?TranslatedPage $translation): string
    {
        if ($translation && $translation->exists && $translation->id) {
            return strtolower((string) $translation->language_code) . ':t:' . $translation->id;
        }

        return 'en:page:' . $globalPage->id;
    }

    public function hasFranchisePageUpdateAvailable(Page $globalPage, ?TranslatedPage $franchiseTranslation, string $evaluationPreferredLanguage): bool
    {
        if (!$franchiseTranslation || !$this->franchiseRowEligibleForGlobalVersionTracking($franchiseTranslation)) {
            return false;
        }

        $current = $this->getCurrentGlobalSourceTranslation($globalPage, $evaluationPreferredLanguage, false);
        if (!$current) {
            return false;
        }

        $storedKey = $this->franchiseStoredSourceKey($franchiseTranslation, $globalPage);
        $currentKey = $this->globalSourceKeyForTranslation($globalPage, $current);

        $storedVer = (int) ($franchiseTranslation->source_global_version ?? 0);
        $currentVer = (int) ($current->version ?? 1);

        if ($storedKey !== $currentKey) {
            return true;
        }

        return $storedVer < $currentVer;
    }

    public function syncStateAgainstCurrentGlobal(Page $globalPage, ?TranslatedPage $franchiseTranslation, string $evaluationPreferredLanguage): string
    {
        if (!$franchiseTranslation || !$this->franchiseRowEligibleForGlobalVersionTracking($franchiseTranslation)) {
            return self::STATE_NOT_ADOPTED;
        }

        $current = $this->getCurrentGlobalSourceTranslation($globalPage, $evaluationPreferredLanguage, false);
        if (!$current) {
            return self::STATE_NOT_ADOPTED;
        }

        $storedKey = $this->franchiseStoredSourceKey($franchiseTranslation, $globalPage);
        $currentKey = $this->globalSourceKeyForTranslation($globalPage, $current);

        $sourceVersion = (int) ($franchiseTranslation->source_global_version ?? 0);
        $globalVersion = (int) ($current->version ?? 1);

        if ($storedKey !== $currentKey) {
            return !is_null($franchiseTranslation->last_local_edit_at)
                ? self::STATE_LOCALLY_MODIFIED
                : self::STATE_OUTDATED;
        }

        if ($sourceVersion < $globalVersion && !is_null($franchiseTranslation->last_local_edit_at)) {
            return self::STATE_LOCALLY_MODIFIED;
        }

        if ($sourceVersion < $globalVersion) {
            return self::STATE_OUTDATED;
        }

        return self::STATE_UP_TO_DATE;
    }

    /**
     * @return array<string, mixed>
     */
    public function resolveSaveSourceAttributionForNewFranchiseTranslation(Page $globalPage, string $targetLanguageCode): array
    {
        $targetLanguageCode = strtolower(trim((string) $targetLanguageCode));
        $sourceLang = $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($targetLanguageCode);

        if ($sourceLang !== 'en') {
            $glPref = $this->resolveGlobalTranslatedPage($globalPage, $sourceLang);
            $prefContent = is_array($glPref?->translated_content) ? $glPref->translated_content : [];
            if ($glPref && $this->translationHasRenderableContent($prefContent)) {
                return [
                    'source_global_page_id' => $globalPage->id,
                    'source_global_translation_id' => $glPref->id,
                    'source_language_code' => $sourceLang,
                    'source_global_version' => max(1, (int) ($glPref->version ?? 1)),
                    'adopted_from_global' => true,
                ];
            }
        }

        $glEn = $this->resolveGlobalTranslatedPage($globalPage, 'en');
        if ($glEn && $glEn->exists) {
            return [
                'source_global_page_id' => $globalPage->id,
                'source_global_translation_id' => $glEn->id,
                'source_language_code' => 'en',
                'source_global_version' => max(1, (int) ($glEn->version ?? 1)),
                'adopted_from_global' => true,
            ];
        }

        $virtual = $this->virtualEnglishGlobalTranslation($globalPage);

        return [
            'source_global_page_id' => $globalPage->id,
            'source_global_translation_id' => null,
            'source_language_code' => 'en',
            'source_global_version' => max(1, (int) ($virtual->version ?? 1)),
            'adopted_from_global' => true,
        ];
    }

    private function translationHasRenderableContent(array $content): bool
    {
        if ($content === []) {
            return false;
        }

        foreach ($content as $section) {
            if (!is_array($section)) {
                if ($section !== null && $section !== '') {
                    return true;
                }

                continue;
            }
            foreach ($section as $value) {
                if ($value === null || $value === '' || $value === []) {
                    continue;
                }
                if (is_array($value) && $value === []) {
                    continue;
                }

                return true;
            }
        }

        return false;
    }

    /**
     * Legacy installs may not have a persisted English translated_pages row; use page fields for read/compare.
     */
    public function virtualEnglishGlobalTranslation(Page $globalPage): TranslatedPage
    {
        $virtual = new TranslatedPage([
            'franchisee_id' => null,
            'page_id' => $globalPage->id,
            'language_code' => 'en',
            'translated_content' => $globalPage->content ?? [],
            'status' => $globalPage->status,
            'seo_title' => $globalPage->seo_title,
            'seo_description' => $globalPage->seo_description,
            'is_global_suggestion' => false,
            'version' => $this->resolveEnglishVirtualGlobalVersion($globalPage),
        ]);
        $virtual->exists = false;

        return $virtual;
    }

    public function resolveEnglishVirtualGlobalVersion(Page $globalPage): int
    {
        $max = (int) TranslatedPage::query()
            ->where('page_id', $globalPage->id)
            ->whereNull('franchisee_id')
            ->max('version');

        return max(1, $max);
    }

    /**
     * Keeps a global translated_pages row for English in sync with the base pages row (version + FK targets).
     */
    /**
     * @param  bool  $promotingDraft  True only when this call IS the publish of the English draft.
     *                                Incidental callers (template-change re-sync, franchise-copy
     *                                bootstrap) must leave a pending draft alone.
     */
    public function syncEnglishMirrorFromPage(Page $page, bool $promotingDraft = false): TranslatedPage
    {
        return DB::transaction(function () use ($page, $promotingDraft) {
            $translation = TranslatedPage::firstOrNew([
                'page_id' => $page->id,
                'language_code' => 'en',
                'franchisee_id' => null,
            ]);

            // An unpublished base page must never overwrite the published mirror — that is what
            // used to destroy the live content and take the page offline. Stage it instead.
            // (Callers should not be writing pages.content on a draft save at all; this is a
            // belt-and-braces guard for any path that still does.)
            if (!$this->isPublished((string) $page->status)) {
                if (!$translation->exists) {
                    $this->initialiseUnpublishedTranslation($translation);
                    $translation->is_global_suggestion = false;
                }

                $this->stageDraftOnTranslation($translation, [
                    'translated_content' => $page->content ?? [],
                    'seo_title' => $page->seo_title,
                    'seo_description' => $page->seo_description,
                ]);

                $translation->save();

                return $translation;
            }

            $dirty = $translation->exists
                ? (
                    json_encode($translation->translated_content) !== json_encode($page->content)
                    || ($translation->seo_title ?? null) !== ($page->seo_title ?? null)
                    || ($translation->seo_description ?? null) !== ($page->seo_description ?? null)
                    || strtolower((string) $translation->status) !== strtolower((string) $page->status)
                )
                : true;

            $translation->translated_content = $page->content ?? [];
            $translation->status = $page->status;
            $translation->seo_title = $page->seo_title;
            $translation->seo_description = $page->seo_description;
            $translation->is_global_suggestion = false;

            // Only an actual publish supersedes the working copy. Incidental re-syncs must not
            // silently discard a draft the editor has not published yet.
            if ($promotingDraft) {
                $this->clearDraftOnTranslation($translation);
            }

            if (!$translation->exists) {
                $translation->version = 1;
            } elseif ($dirty && $this->isPublished($translation->status)) {
                $translation->version = max(1, (int) $translation->version) + 1;
            }

            $translation->save();

            if ($dirty && $this->isPublished($translation->status)) {
                $this->incrementAllGlobalTranslatedPageVersionsExcept($page->id, 'en');
            }

            // The English mirror never snapshotted, so global English pages had no version
            // history at all and nothing to roll back to.
            if ($this->isPublished($translation->status)) {
                $this->snapshotPageVersion($translation);
            }

            return $translation;
        });
    }

    public function resolveFranchiseTranslatedPage(string $globalPageId, string $franchiseeId, string $language): ?TranslatedPage
    {
        $language = strtolower($language);
        $storagePageId = $this->resolveTranslatedPageStorageId($globalPageId, $franchiseeId);

        return TranslatedPage::query()
            ->where('page_id', $storagePageId)
            ->where('franchisee_id', $franchiseeId)
            ->where('language_code', $language)
            ->first();
    }

    public function syncState(?TranslatedPage $franchiseTranslation, ?TranslatedPage $globalTranslation): string
    {
        if (!$franchiseTranslation || !$globalTranslation) {
            return self::STATE_NOT_ADOPTED;
        }

        if (!$this->franchiseRowEligibleForGlobalVersionTracking($franchiseTranslation)) {
            return self::STATE_NOT_ADOPTED;
        }

        $sourceVersion = (int) ($franchiseTranslation->source_global_version ?? 0);
        $globalVersion = (int) ($globalTranslation->version ?? 1);

        if ($sourceVersion < $globalVersion && !is_null($franchiseTranslation->last_local_edit_at)) {
            return self::STATE_LOCALLY_MODIFIED;
        }

        if ($sourceVersion < $globalVersion) {
            return self::STATE_OUTDATED;
        }

        return self::STATE_UP_TO_DATE;
    }

    public function hasUpdateAvailable(?TranslatedPage $franchiseTranslation, ?TranslatedPage $globalTranslation): bool
    {
        if (!$franchiseTranslation || !$globalTranslation) {
            return false;
        }

        return (int) ($franchiseTranslation->source_global_version ?? 0) < (int) ($globalTranslation->version ?? 1)
            && $this->franchiseRowEligibleForGlobalVersionTracking($franchiseTranslation);
    }

    public function saveGlobalTranslatedPage(Page $page, string $languageCode, array $attributes): TranslatedPage
    {
        return DB::transaction(function () use ($page, $languageCode, $attributes) {
            $languageCode = strtolower($languageCode);

            $translation = TranslatedPage::firstOrNew([
                'page_id' => $page->id,
                'language_code' => $languageCode,
                'franchisee_id' => null,
            ]);

            // Draft: stage the edits and leave the published copy, status and version untouched.
            if ($this->isDraftSave($attributes)) {
                if (!$translation->exists) {
                    $this->initialiseUnpublishedTranslation($translation);
                    $translation->is_global_suggestion = (bool) ($attributes['is_global_suggestion'] ?? false);
                }

                $this->stageDraftOnTranslation($translation, $attributes);
                $translation->save();

                return $translation;
            }

            $dirtyIncoming = $translation->exists
                ? $this->hasTranslatableChanges($translation, $attributes)
                : true;

            $translation->fill(Arr::only($attributes, self::TRANSLATABLE_TRANSLATED_PAGE_FIELDS));

            // Publishing promotes the working copy, so the draft buffer is spent.
            $this->clearDraftOnTranslation($translation);

            if (!$translation->exists) {
                $translation->version = 1;
            } elseif ($dirtyIncoming && $this->isPublished($translation->status)) {
                $translation->version = max(1, (int) $translation->version) + 1;
            }

            $translation->save();

            if ($this->isPublished($translation->status ?? '')) {
                $this->snapshotPageVersion($translation);
            }

            return $translation;
        });
    }

    public function incrementAllGlobalTranslatedPageVersions(string $pageId): void
    {
        TranslatedPage::query()
            ->where('page_id', $pageId)
            ->whereNull('franchisee_id')
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->each(function (TranslatedPage $row) {
                $row->version = max(1, (int) $row->version) + 1;
                $row->save();
            });
    }

    /**
     * Super-admin template or layout changes do not always alter translated_content, so
     * syncEnglishMirrorFromPage may not bump versions. Increment all global rows for every
     * global page that uses this template so franchises see Update available / Reset.
     */
    public function bumpGlobalTranslationVersionsForPagesUsingTemplate(int|string $templateId): void
    {
        Page::query()
            ->where('template_id', $templateId)
            ->where('is_global', true)
            ->whereNull('franchisee_id')
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->each(function (Page $globalPage) {
                $fresh = $globalPage->fresh() ?? $globalPage;
                $this->syncEnglishMirrorFromPage($fresh);
                $this->incrementAllGlobalTranslatedPageVersions($fresh->id);
            });
    }

    private function incrementAllGlobalTranslatedPageVersionsExcept(string $pageId, string $exceptLanguage): void
    {
        $exceptLanguage = strtolower($exceptLanguage);

        TranslatedPage::query()
            ->where('page_id', $pageId)
            ->whereNull('franchisee_id')
            ->whereNull('deleted_at')
            ->where('language_code', '!=', $exceptLanguage)
            ->orderBy('id')
            ->each(function (TranslatedPage $row) {
                $row->version = max(1, (int) $row->version) + 1;
                $row->save();
            });
    }

    public function saveFranchiseManualTranslatedPage(
        Page $storagePage,
        string $franchiseeId,
        string $languageCode,
        array $attributes,
    ): TranslatedPage {
        return DB::transaction(function () use ($storagePage, $franchiseeId, $languageCode, $attributes) {
            $languageCode = strtolower($languageCode);
            $globalPage = $this->resolveGlobalRootPage($storagePage);

            $translation = TranslatedPage::firstOrNew([
                'page_id' => $storagePage->id,
                'franchisee_id' => $franchiseeId,
                'language_code' => $languageCode,
            ]);

            $globalTranslation = $this->resolveGlobalTranslatedPage($globalPage, $languageCode);
            if (!$globalTranslation && $languageCode !== 'en') {
                $globalTranslation = $this->resolveGlobalTranslatedPage($globalPage, 'en');
            }

            if ($translation->exists && $translation->source_language_code === null && $languageCode !== 'en') {
                $translation->fill($this->resolveSaveSourceAttributionForNewFranchiseTranslation($globalPage, $languageCode));
            }

            if (!$translation->exists) {
                $translation->fill($this->resolveSaveSourceAttributionForNewFranchiseTranslation($globalPage, $languageCode));

                if (!$globalTranslation) {
                    $virtual = $this->virtualEnglishGlobalTranslation($globalPage);
                    $translation->translated_content = $virtual->translated_content;
                    $translation->status = $virtual->status;
                    $translation->seo_title = $virtual->seo_title;
                    $translation->seo_description = $virtual->seo_description;
                    $translation->is_global_suggestion = false;
                } else {
                    $translation->translated_content = $globalTranslation->translated_content;
                    $translation->status = $globalTranslation->status;
                    $translation->is_global_suggestion = (bool) ($globalTranslation->is_global_suggestion ?? false);
                }
                $translation->version = 1;
            }

            // Draft: stage the edits only. The published copy, status and version stay put, and
            // crucially so do last_local_edit_at / adopted_from_global — an unpublished draft must
            // not flip the row to "locally modified" and change what the sync UI reports.
            if ($this->isDraftSave($attributes)) {
                $this->stageDraftOnTranslation($translation, $attributes);
                $translation->save();

                return $translation;
            }

            $dirtyIncoming = $translation->exists
                ? $this->hasTranslatableChanges($translation, $attributes)
                : true;

            $translation->fill(Arr::only($attributes, self::TRANSLATABLE_TRANSLATED_PAGE_FIELDS));

            // Publishing promotes the working copy, so the draft buffer is spent.
            $this->clearDraftOnTranslation($translation);

            if (!$translation->exists) {
                $translation->version = 1;
            }

            if ($dirtyIncoming) {
                $translation->last_local_edit_at = Carbon::now();
                $translation->adopted_from_global = false;
            }

            // Increment version counter when publishing updated content.
            if ($dirtyIncoming && $this->isPublished($translation->status ?? '')) {
                $translation->version = max(1, (int) $translation->version) + 1;
            }

            $translation->save();

            // Snapshot to page_version_history so old versions remain auditable.
            if ($this->isPublished($translation->status ?? '')) {
                $this->snapshotPageVersion($translation);
            }

            return $translation;
        });
    }

    private function snapshotPageVersion(TranslatedPage $translation): void
    {
        try {
            $page = $translation->page ?? Page::find($translation->page_id);

            PageVersionHistory::firstOrCreate(
                [
                    'page_id'       => $translation->page_id,
                    'franchisee_id' => $translation->franchisee_id,
                    'language_code' => $translation->language_code,
                    'version'       => $translation->version,
                ],
                [
                    'title'        => $page?->title,
                    'content'      => $translation->translated_content,
                    'published_at' => now(),
                    'published_by' => Auth::id(),
                ]
            );
        } catch (\Throwable) {
            // Snapshot failure must never break the main publish flow.
        }
    }

    public function adoptGlobal(Page $targetPage, string $language, string $franchiseeId): TranslatedPage
    {
        return $this->copyGlobalIntoFranchise($targetPage, $language, $franchiseeId);
    }

    public function resetToGlobal(Page $targetPage, string $language, string $franchiseeId): TranslatedPage
    {
        return $this->copyGlobalIntoFranchise($targetPage, $language, $franchiseeId);
    }

    public function updateToLatestGlobal(Page $targetPage, string $language, string $franchiseeId): TranslatedPage
    {
        return $this->copyGlobalIntoFranchise($targetPage, $language, $franchiseeId);
    }

    private function copyGlobalPageBasicsOntoFranchiseStoragePage(Page $storagePage, Page $globalPage, mixed $prevPageStatus): void
    {
        $storagePage->title = $globalPage->title;
        $storagePage->slug = $globalPage->slug;
        $storagePage->seo_title = $globalPage->seo_title;
        $storagePage->seo_description = $globalPage->seo_description;
        $storagePage->content = $globalPage->content;
        if ($storagePage->franchisee_id) {
            if ($prevPageStatus !== null) {
                $storagePage->status = $prevPageStatus;
            }
        } else {
            $storagePage->status = $globalPage->status;
        }
        $storagePage->save();
    }

    private function copyGlobalIntoFranchise(Page $targetPage, string $language, string $franchiseeId): TranslatedPage
    {
        return DB::transaction(function () use ($targetPage, $language, $franchiseeId) {
            $globalPage = $this->resolveGlobalRootPage($targetPage);
            $language = strtolower($language);

            if ($language === 'en') {
                $globalEn = TranslatedPage::query()
                    ->where('page_id', $globalPage->id)
                    ->whereNull('franchisee_id')
                    ->where('language_code', 'en')
                    ->first();

                if (!$globalEn) {
                    $globalEn = $this->syncEnglishMirrorFromPage($globalPage);
                }

                $storagePage = $this->ensureFranchiseOverridePage($globalPage, $franchiseeId);
                $franchiseTranslation = TranslatedPage::firstOrNew([
                    'page_id' => $storagePage->id,
                    'franchisee_id' => $franchiseeId,
                    'language_code' => 'en',
                ]);

                $prevTranslatedStatus = $franchiseTranslation->exists ? $franchiseTranslation->status : null;
                $prevPageStatus = $storagePage->status;

                $franchiseTranslation->translated_content = $globalEn->translated_content;
                $franchiseTranslation->status = $prevTranslatedStatus ?? $globalEn->status;
                $franchiseTranslation->seo_title = $globalEn->seo_title;
                $franchiseTranslation->seo_description = $globalEn->seo_description;
                $franchiseTranslation->is_global_suggestion = false;
                $franchiseTranslation->adopted_from_global = true;
                $franchiseTranslation->source_global_page_id = $globalPage->id;
                $franchiseTranslation->source_language_code = 'en';
                $franchiseTranslation->source_global_translation_id = $globalEn->id;
                $franchiseTranslation->source_global_version = max(1, (int) ($globalEn->version ?? 1));
                $franchiseTranslation->last_local_edit_at = null;
                $franchiseTranslation->version = max(1, (int) ($franchiseTranslation->version ?? 1));
                // The row has just been overwritten from global, so any franchise draft that was
                // pending against the OLD content is stale — drop it rather than leave it to
                // resurface in preview.
                $this->clearDraftOnTranslation($franchiseTranslation);
                $franchiseTranslation->save();

                $this->copyGlobalPageBasicsOntoFranchiseStoragePage($storagePage, $globalPage, $prevPageStatus);

                return $franchiseTranslation;
            }

            $globalTranslation = $this->getCurrentGlobalSourceTranslation($globalPage, $language, false);
            if (!$globalTranslation) {
                throw new \RuntimeException('Global translation could not be resolved for franchise sync.');
            }

            $sourceLanguageCode = strtolower((string) ($globalTranslation->language_code ?? 'en'));
            $storagePage = $this->ensureFranchiseOverridePage($globalPage, $franchiseeId);

            $franchiseTranslation = TranslatedPage::firstOrNew([
                'page_id' => $storagePage->id,
                'franchisee_id' => $franchiseeId,
                'language_code' => $language,
            ]);

            $prevTranslatedStatus = $franchiseTranslation->exists ? $franchiseTranslation->status : null;
            $prevPageStatus = $storagePage->status;

            $franchiseTranslation->fill(Arr::only($globalTranslation->toArray(), self::TRANSLATABLE_TRANSLATED_PAGE_FIELDS));
            $franchiseTranslation->status = $prevTranslatedStatus ?? $globalTranslation->status;
            $franchiseTranslation->source_global_page_id = $globalPage->id;
            $franchiseTranslation->source_language_code = $sourceLanguageCode;
            $franchiseTranslation->source_global_translation_id = $globalTranslation->id ?: null;
            $franchiseTranslation->source_global_version = max(1, (int) ($globalTranslation->version ?? 1));
            $franchiseTranslation->adopted_from_global = true;
            $franchiseTranslation->last_local_edit_at = null;
            $franchiseTranslation->version = max(1, (int) ($franchiseTranslation->version ?? 1));
            // Overwritten from global: a draft pending against the old content is stale.
            $this->clearDraftOnTranslation($franchiseTranslation);
            $franchiseTranslation->save();

            $this->copyGlobalPageBasicsOntoFranchiseStoragePage($storagePage, $globalPage, $prevPageStatus);

            return $franchiseTranslation;
        });
    }

    private function hasTranslatableChanges(TranslatedPage $existing, array $incoming): bool
    {
        foreach (self::TRANSLATABLE_TRANSLATED_PAGE_FIELDS as $field) {
            if (!array_key_exists($field, $incoming)) {
                continue;
            }

            if ($existing->{$field} != $incoming[$field]) {
                return true;
            }
        }

        return false;
    }

    private function isPublished(?string $status): bool
    {
        return in_array(strtolower((string) $status), ['publish', 'published', 'public'], true);
    }
}
