<?php

namespace App\Http\Controllers\Admin;

use App\Helpers\ApiResponse;
use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use App\Models\Form;
use App\Models\FormNotification;
use App\Models\FormNotificationTranslation;
use App\Models\FormTranslation;
use App\Services\FormContentNormalizerService;
use App\Services\FormNotificationTranslationDiffService;
use App\Services\FormNotificationTranslationSyncService;
use App\Services\FormTranslationDiffService;
use App\Services\FormTranslationSyncService;
use App\Services\FranchiseGlobalSourceResolver;
use App\Services\TranslationDiff\MeaningfulContentDiffService;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class FormController extends Controller
{
    private const GLOBAL_DELETED_MESSAGE = 'The original global form was deleted by super admin. Your franchise override is still available, but it is no longer linked to active global content.';

    public function __construct(
        private readonly FormTranslationSyncService $formTranslationSyncService,
        private readonly FormNotificationTranslationSyncService $formNotificationTranslationSyncService,
        private readonly FormContentNormalizerService $formContentNormalizerService,
        private readonly FormTranslationDiffService $formTranslationDiffService,
        private readonly FormNotificationTranslationDiffService $formNotificationTranslationDiffService,
        private readonly MeaningfulContentDiffService $meaningfulContentDiffService,
        private readonly FranchiseGlobalSourceResolver $franchiseGlobalSourceResolver,
    ) {
    }

    public function index(Request $request)
    {
        return $this->getAllForms($request);
    }

    public function getAllForms(Request $request)
    {
        $auth = Auth::user();
        $language = strtolower((string) $request->get('language', ''));
        if ($language === '') {
            $language = strtolower((string) ($auth->franchisee?->language ?? 'en'));
        }
        if ($language === 'english') {
            $language = 'en';
        }
        $perPage = (int) $request->get('per_page', 15);
        $sortBy = $request->get('sort_by', 'order_no');
        $sortOrder = $request->get('sort_order') == '-1' ? 'desc' : 'asc';
        $search = (string) $request->get('search', '');

        $bases = $this->resolveAccessibleBaseForms($auth)->load([
            'translations',
            'notifications.translations',
            'parentForm.translations',
            'parentForm.notifications.translations',
        ]);

        $rows = $bases->map(fn(Form $form) => $this->projectForm($form, $language, $auth->role === 'super_admin' ? null : $auth->franchisee_id));

        if ($search !== '') {
            $rows = $rows->filter(fn(array $row) => str_contains(strtolower((string) ($row['name'] ?? '')), strtolower($search)));
        }

        $allowedSort = ['name', 'order_no', 'status', 'created_at'];
        $sortBy = in_array($sortBy, $allowedSort, true) ? $sortBy : 'order_no';
        $rows = $rows->sortBy($sortBy, SORT_NATURAL, $sortOrder === 'desc')->values();

        $page = (int) $request->get('page', 1);

        $paginated = new LengthAwarePaginator(
            $rows->forPage($page, $perPage)->values(),
            $rows->count(),
            $perPage,
            $page,
            ['path' => $request->url(), 'query' => $request->query()]
        );

        return ApiResponse::success($paginated, 'Forms fetched successfully');
    }

    public function store(Request $request)
    {
        return $this->saveForm($request);
    }

    public function saveForm(Request $request)
    {
        $auth = Auth::user();

        $validator = Validator::make($request->all(), [
            'language' => 'nullable|string|max:10',
            'name' => 'required|string|max:191',
            'description' => 'nullable|string',
            'fields' => 'nullable|array',
            'pages' => 'nullable|array',
            'status' => 'nullable|in:Published,Draft',
            'translated' => 'nullable|boolean',

            'form_setting' => 'nullable|array',
            'is_required_for_translation' => 'nullable|boolean',
            'order_no' => 'nullable|integer',

            'notifications' => 'nullable|array',
            'notifications.*.id' => 'nullable|uuid',
            'notifications.*.notification_name' => 'required|string|max:191',
            'notifications.*.to_email' => 'nullable|string|max:191',
            'notifications.*.from_name' => 'nullable|string|max:191',
            'notifications.*.from_email' => 'nullable|string|max:191',
            'notifications.*.cc' => 'nullable|string',
            'notifications.*.bcc' => 'nullable|string',
            'notifications.*.event_key' => 'required|string|max:191',
            'notifications.*.subject' => 'nullable|string|max:255',
            'notifications.*.message' => 'nullable|string',
            'notifications.*.status' => 'nullable|in:Published,Draft',
        ]);

        if ($validator->fails()) {
            return ApiResponse::error($validator->errors()->first(), Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $data = $validator->validated();
        $language = strtolower((string) ($data['language'] ?? 'en'));

        try {
            $form = null;
            DB::transaction(function () use ($auth, $data, $language, &$form) {
            $form = Form::create([
                'franchisee_id' => $auth->role === 'super_admin' ? null : $auth->franchisee_id,
                'parent_form_id' => null,
                'is_global' => $auth->role === 'super_admin',
                'form_setting' => $data['form_setting'] ?? null,
                'is_required_for_translation' => (bool) ($data['is_required_for_translation'] ?? false),
                'order_no' => $data['order_no'] ?? null,
            ]);

            $translationPayload = $this->formContentNormalizerService->normalizeTranslationPayload([
                'name' => $data['name'] ?? null,
                'description' => $data['description'] ?? null,
                'fields' => $data['fields'] ?? null,
                'pages' => $data['pages'] ?? null,
                'form_setting' => $data['form_setting'] ?? null,
                'status' => $data['status'] ?? 'Draft',
                'translated' => (bool) ($data['translated'] ?? false),
            ]);

            if ($auth->role === 'super_admin') {
                $this->formTranslationSyncService->saveGlobalTranslation($form, $language, $translationPayload);
            } else {
                $this->formTranslationSyncService->saveFranchiseManualTranslation($form, $language, $translationPayload);
            }

            $this->upsertNotifications($form, $data['notifications'] ?? [], $language, $auth->role === 'super_admin');
            });

            CacheHelper::forgetByTag('forms');

            $form->load(['translations', 'notifications.translations', 'parentForm']);

            return ApiResponse::success(
                $this->projectForm($form, $language, $auth->role === 'super_admin' ? null : $auth->franchisee_id),
                'Form created successfully'
            );
        } catch (\Throwable $e) {
            // saveForm previously had no error handling, so any failure surfaced as
            // an opaque 500 "Server Error". Log the real cause and return it.
            report($e);
            Log::error('Form creation failed', [
                'error' => $e->getMessage(),
                'file'  => $e->getFile() . ':' . $e->getLine(),
            ]);

            return ApiResponse::error(
                'Failed to create form: ' . $e->getMessage(),
                Response::HTTP_INTERNAL_SERVER_ERROR
            );
        }
    }

    public function show(string $id, Request $request)
    {
        return $this->getFormById($id, $request);
    }

    public function getFormById(string $id, Request $request)
    {
        if (!Str::isUuid($id)) {
            return ApiResponse::error('Invalid form ID.', Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $auth = Auth::user();
        $language = strtolower((string) $request->get('language', ''));
        if ($language === '') {
            $language = strtolower((string) ($auth->franchisee?->language ?? 'en'));
        }
        if ($language === 'english') {
            $language = 'en';
        }

        $base = Form::withTrashed()->with([
            'translations',
            'notifications.translations',
            'parentForm.translations',
            'parentForm.notifications.translations',
        ])->findOrFail($id);

        if ($auth->role !== 'super_admin' && $base->is_global) {
            $override = Form::withTrashed()
                ->where('franchisee_id', $auth->franchisee_id)
                ->where('parent_form_id', $base->id)
                ->with(['translations', 'notifications.translations', 'parentForm.translations', 'parentForm.notifications.translations'])
                ->first();
            $base = $override ?: $base;
        }

        $franchiseeId = $auth->role === 'super_admin' ? null : $auth->franchisee_id;
        $availableLanguages = $this->resolveAvailableLanguages($base);

        $projected = $this->projectForm($base, $language, $franchiseeId, $availableLanguages);
        $projected['translation_family'] = collect($availableLanguages)
            ->map(fn(string $lang) => $this->projectForm($base, $lang, $franchiseeId, $availableLanguages))
            ->values()
            ->all();

        return ApiResponse::success($projected, 'Form fetched successfully');
    }

    public function update(Request $request, string $id)
    {
        return $this->updateForm($request, $id);
    }

    public function updateForm(Request $request, string $id)
    {
        if (!Str::isUuid($id)) {
            return ApiResponse::error('Invalid form ID.', Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $auth = Auth::user();
        $language = strtolower((string) $request->get('language', 'en'));
        if ($language === 'english') {
            $language = 'en';
        }

        $base = Form::withTrashed()->with(['translations', 'notifications.translations', 'parentForm'])->findOrFail($id);

        if ($auth->role !== 'super_admin' && $base->is_global) {
            $base = Form::firstOrCreate([
                'franchisee_id' => $auth->franchisee_id,
                'parent_form_id' => $base->id,
            ], [
                'is_global' => false,
                'form_setting' => $base->form_setting,
                'is_required_for_translation' => $base->is_required_for_translation,
                'order_no' => $base->order_no,
            ]);

            if (!FormTranslation::where('form_id', $base->id)->where('language', $language)->exists()) {
                $this->formTranslationSyncService->adoptGlobal($base, $language);
            }
        }

        $validator = Validator::make($request->all(), [
            'name' => 'sometimes|required|string|max:191',
            'description' => 'nullable|string',
            'fields' => 'nullable|array',
            'pages' => 'nullable|array',
            'status' => 'nullable|in:Published,Draft',
            'translated' => 'nullable|boolean',
            'form_setting' => 'nullable|array',
            'is_required_for_translation' => 'nullable|boolean',
            'order_no' => 'nullable|integer',
            'notifications' => 'nullable|array',
            'notifications.*.id' => 'nullable|uuid',
            'notifications.*.notification_name' => 'required|string|max:191',
            'notifications.*.to_email' => 'nullable|string|max:191',
            'notifications.*.from_name' => 'nullable|string|max:191',
            'notifications.*.from_email' => 'nullable|string|max:191',
            'notifications.*.cc' => 'nullable|string',
            'notifications.*.bcc' => 'nullable|string',
            'notifications.*.event_key' => 'required|string|max:191',
            'notifications.*.subject' => 'nullable|string|max:255',
            'notifications.*.message' => 'nullable|string',
            'notifications.*.status' => 'nullable|in:Published,Draft',
        ]);

        if ($validator->fails()) {
            return ApiResponse::error($validator->errors()->first(), Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $data = $validator->validated();
        $hasFormSettingUpdate = array_key_exists('form_setting', $data);

        DB::transaction(function () use ($base, $data, $language, $auth, $hasFormSettingUpdate) {
            $base->update([
                'form_setting' => $hasFormSettingUpdate ? $data['form_setting'] : $base->form_setting,
                'is_required_for_translation' => array_key_exists('is_required_for_translation', $data)
                    ? (bool) $data['is_required_for_translation']
                    : $base->is_required_for_translation,
                'order_no' => $data['order_no'] ?? $base->order_no,
            ]);

            if ($hasFormSettingUpdate && is_null($base->franchisee_id) && $base->is_global) {
                $this->formTranslationSyncService->touchGlobalSettingVersion($base, $data['form_setting'], $language);
            }

            $translationPayload = [];
            foreach (['name', 'description', 'fields', 'pages', 'form_setting', 'status', 'translated'] as $key) {
                if (array_key_exists($key, $data)) {
                    $translationPayload[$key] = $data[$key];
                }
            }
            $translationPayload = $this->formContentNormalizerService->normalizeTranslationPayload($translationPayload);

            if (!empty($translationPayload)) {
                if (is_null($base->franchisee_id) && $base->is_global) {
                    $this->formTranslationSyncService->saveGlobalTranslation($base, $language, $translationPayload);
                } else {
                    $this->formTranslationSyncService->saveFranchiseManualTranslation($base, $language, $translationPayload);
                }
            }

            if (array_key_exists('notifications', $data)) {
                $this->upsertNotifications($base, $data['notifications'] ?? [], $language, is_null($base->franchisee_id));
            }
        });

        CacheHelper::forgetByTag('forms');

        $base->refresh()->load(['translations', 'notifications.translations', 'parentForm.translations', 'parentForm.notifications.translations']);

        return ApiResponse::success(
            $this->projectForm($base, $language, $auth->role === 'super_admin' ? null : $auth->franchisee_id),
            'Form updated successfully'
        );
    }

    public function destroy(string $id, Request $request)
    {
        if (!Str::isUuid($id)) {
            return ApiResponse::error('Invalid form ID.', Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $auth = Auth::user();
        $language = strtolower((string) $request->get('language', 'en'));
        $base = Form::withTrashed()->findOrFail($id);

        if ($auth->role !== 'super_admin' && $base->is_global) {
            return ApiResponse::error('Unauthorized: Franchisee cannot delete global form base', Response::HTTP_FORBIDDEN);
        }

        if ($auth->role !== 'super_admin' && $base->franchisee_id !== $auth->franchisee_id) {
            return ApiResponse::error('Unauthorized: You cannot delete another franchisee form', Response::HTTP_FORBIDDEN);
        }

        if ($language !== 'en') {
            FormTranslation::where('form_id', $base->id)->where('language', $language)->delete();

            $notificationIds = $base->notifications()->pluck('id');
            FormNotificationTranslation::whereIn('form_notification_id', $notificationIds)
                ->where('language', $language)
                ->delete();

            CacheHelper::forgetByTag('forms');

            return ApiResponse::success([], 'Form translation deleted successfully');
        }

        DB::transaction(function () use ($base) {
            $notificationIds = $base->notifications()->pluck('id');
            FormNotificationTranslation::whereIn('form_notification_id', $notificationIds)->delete();
            FormTranslation::where('form_id', $base->id)->delete();
            $base->notifications()->delete();
            $base->delete();
        });

        CacheHelper::forgetByTag('forms');

        return ApiResponse::success([], 'Form deleted successfully');
    }

    public function reorder(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'forms' => 'required|array',
            'forms.*.id' => 'required|uuid',
            'forms.*.order_no' => 'required|integer',
        ]);

        if ($validator->fails()) {
            return ApiResponse::error($validator->errors()->first(), Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $auth = Auth::user();

        DB::transaction(function () use ($request, $auth) {
            foreach ($request->forms as $item) {
                $query = Form::where('id', $item['id']);
                if ($auth->role !== 'super_admin') {
                    $query->where('franchisee_id', $auth->franchisee_id);
                } else {
                    $query->whereNull('franchisee_id');
                }

                $query->update(['order_no' => (int) $item['order_no']]);
            }
        });

        CacheHelper::forgetByTag('forms');

        return ApiResponse::success([], 'Form order updated successfully');
    }

    private function formPreviewRowIsFormBody(array $row): bool
    {
        $t = (string) ($row['label'] ?? $row['field'] ?? '');

        return str_starts_with($t, 'Form Details') || str_starts_with($t, 'Form Fields');
    }

    private function formPreviewRowIsNotificationBody(array $row): bool
    {
        $t = (string) ($row['label'] ?? $row['field'] ?? '');

        return str_starts_with($t, 'Notifications');
    }

    public function updatePreview(Request $request, string $id)
    {
        try {
            $auth = Auth::user();
            if (!$auth || $auth->role === 'super_admin' || !$auth->franchisee_id) {
                return ApiResponse::error('Only franchise users can perform this action.', Response::HTTP_FORBIDDEN);
            }

            if (!Str::isUuid($id)) {
                return ApiResponse::error('Invalid form ID.', Response::HTTP_UNPROCESSABLE_ENTITY);
            }

            $requestedLanguage = strtolower((string) $request->get('language', ''));
            if ($requestedLanguage === '') {
                $requestedLanguage = strtolower((string) ($auth->franchisee?->language ?? 'en'));
            }
            $language = $requestedLanguage === 'english' ? 'en' : ($requestedLanguage ?: 'en');

            $form = Form::withTrashed()->with([
                'translations',
                'notifications.translations',
                'parentForm.translations',
                'parentForm.notifications.translations',
            ])->findOrFail($id);

            if (!$form->is_global && $form->franchisee_id && $form->franchisee_id !== $auth->franchisee_id) {
                return ApiResponse::error('Unauthorized: You cannot preview updates for another franchise form.', Response::HTTP_FORBIDDEN);
            }

            $globalForm = $this->resolveGlobalForm($form);
            if (!$globalForm) {
                return ApiResponse::success([
                    'update_available' => false,
                    'language' => $language,
                    'message' => 'Latest global form could not be found.',
                    'changes' => [],
                ]);
            }

            $franchiseTranslation = collect($form->translations ?? [])
                ->first(fn(FormTranslation $t) => strtolower((string) $t->language) === $language);
            $globalTranslation = $this->formTranslationSyncService->resolveGlobalTranslationForFranchisePreferred($globalForm, $language);

            $changes = [];
            $sourceGlobalVersion = (int) ($franchiseTranslation?->source_global_version ?? 0);
            $latestGlobalVersion = (int) ($globalTranslation?->version ?? 0);
            $hasLocalChanges = !is_null($franchiseTranslation?->last_local_edit_at);

            if ($franchiseTranslation && $globalTranslation) {
                $changes = array_merge(
                    $changes,
                    $this->formTranslationDiffService->diff($franchiseTranslation, $globalTranslation)
                );
            }

            $notificationDiff = $this->buildNotificationDiff($form, $globalForm, $language);
            $changes = array_values(array_merge($changes, $notificationDiff['changes']));
            $changes = array_values(array_filter($changes, fn($c) => is_array($c)));

            $meaningful = $this->meaningfulContentDiffService->filter($changes);
            $formBodyMeaningful = count(array_filter($meaningful, fn(array $row) => $this->formPreviewRowIsFormBody($row))) > 0;

            $formVersionOutdated = $franchiseTranslation && $globalTranslation
                && $this->formTranslationSyncService->hasUpdateAvailable($franchiseTranslation, $globalTranslation);
            $formUpdateAvailable = $formVersionOutdated && $formBodyMeaningful;

            $notificationUpdateAvailable = $this->computeNotificationSuperAdminUpdateAvailable($form, $globalForm, $language);

            $updateAvailable = $formUpdateAvailable || $notificationUpdateAvailable;

            $syncState = $this->formTranslationSyncService->syncState($franchiseTranslation, $globalTranslation);

            return ApiResponse::success([
                'update_available' => $updateAvailable,
                'form_update_available' => $formUpdateAvailable,
                'notification_update_available' => $notificationUpdateAvailable,
                'sync_state' => $syncState,
                'language' => $language,
                'source_global_version' => $sourceGlobalVersion,
                'latest_global_version' => $latestGlobalVersion,
                'has_local_changes' => $hasLocalChanges,
                'warning' => 'Accepting this update will replace your current franchise form content with the latest super admin version.',
                'changes' => $meaningful,
            ]);
        } catch (\Throwable $e) {
            return ApiResponse::exception($e);
        }
    }

    /**
     * Build per-notification translation diffs between franchise and global notifications.
     *
     * @return array{changes: array<int, array<string, mixed>>, update_available: bool}
     */
    private function buildNotificationDiff(Form $form, Form $globalForm, string $language): array
    {
        $localByEvent = collect($form->notifications ?? [])->keyBy('event_key');
        $globalByEvent = collect($globalForm->notifications ?? [])->keyBy('event_key');

        $changes = [];

        foreach ($globalByEvent as $eventKey => $globalNotification) {
            $localNotification = $localByEvent->get($eventKey);
            $label = $globalNotification->notification_name ?: $eventKey;

            $effectiveLang = $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($language);
            $globalTranslation = $this->formNotificationTranslationSyncService->resolveGlobalTranslation(
                $globalNotification,
                $effectiveLang
            );

            if (!$globalTranslation) {
                continue;
            }

            if (!$localNotification) {
                $changes[] = [
                    'field' => sprintf('Notifications — %s added', $label),
                    'label' => sprintf('Notifications — %s added', $label),
                    'old' => null,
                    'new' => sprintf('Subject: %s', $globalTranslation->subject ?? ''),
                    'changed' => true,
                    'type' => 'text',
                ];
                continue;
            }

            $localTranslation = collect($localNotification->translations ?? [])
                ->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === $language)
                ?? collect($localNotification->translations ?? [])
                    ->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === 'en');

            if (!$localTranslation) {
                $changes[] = [
                    'field' => sprintf('Notifications — %s subject changed', $label),
                    'label' => sprintf('Notifications — %s subject changed', $label),
                    'old' => null,
                    'new' => $globalTranslation->subject,
                    'changed' => true,
                    'type' => 'text',
                ];
                continue;
            }

            $itemChanges = $this->formNotificationTranslationDiffService->diff(
                $localTranslation,
                $globalTranslation,
                $label
            );

            if (!empty($itemChanges)) {
                $changes = array_merge($changes, $itemChanges);
            }
        }

        return [
            'changes' => array_values($changes),
            'update_available' => false,
        ];
    }

    /**
     * True when any notification translation has a newer global version than the franchise is tracking
     * and there are meaningful content differences.
     */
    private function computeNotificationSuperAdminUpdateAvailable(Form $franchiseForm, Form $globalForm, string $language): bool
    {
        $localByEvent = collect($franchiseForm->notifications ?? [])->keyBy('event_key');
        $globalByEvent = collect($globalForm->notifications ?? [])->keyBy('event_key');
        $eventKeys = $localByEvent->keys()->merge($globalByEvent->keys())->unique()->values();

        foreach ($eventKeys as $eventKey) {
            $localNotification = $localByEvent->get($eventKey);
            $globalNotification = $globalByEvent->get($eventKey);
            if (!$globalNotification instanceof FormNotification) {
                continue;
            }

            $localTranslations = collect($localNotification?->translations ?? []);
            $globalTranslations = collect($globalNotification->translations ?? []);

            $localRequested = $localTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === $language);
            $localEnglish = $localTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === 'en');
            $globalRequested = $globalTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === $language);
            $globalEnglish = $globalTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === 'en');

            if (
                $globalRequested
                && $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($language) !== $language
            ) {
                $globalRequested = null;
            }

            $franchiseSourceTranslation = $localRequested ?: $localEnglish;
            $globalSourceTranslation = $globalRequested ?: $globalEnglish;
            $label = $globalNotification->notification_name ?: (string) $eventKey;

            if (!$franchiseSourceTranslation || !$globalSourceTranslation) {
                continue;
            }

            if (!$this->formNotificationTranslationSyncService->hasUpdateAvailable($franchiseSourceTranslation, $globalSourceTranslation)) {
                continue;
            }

            $diff = $this->formNotificationTranslationDiffService->diff(
                $franchiseSourceTranslation,
                $globalSourceTranslation,
                $label
            );

            if ($this->meaningfulContentDiffService->hasMeaningfulChanges($diff)) {
                return true;
            }
        }

        return false;
    }

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

    public function adoptGlobal(Request $request, string $id)
    {
        return $this->syncFromGlobalAction($request, $id, 'adopt');
    }

    public function resetToGlobal(Request $request, string $id)
    {
        return $this->syncFromGlobalAction($request, $id, 'reset');
    }

    public function updateToLatestGlobal(Request $request, string $id)
    {
        return $this->syncFromGlobalAction($request, $id, 'latest');
    }

    private function syncFromGlobalAction(Request $request, string $id, string $mode)
    {
        $auth = Auth::user();
        if ($auth->role === 'super_admin' || !$auth->franchisee_id) {
            return ApiResponse::error('Only franchise users can perform this action.', Response::HTTP_FORBIDDEN);
        }

        $language = strtolower((string) $request->get('language', 'en'));

        $form = Form::withTrashed()->findOrFail($id);
        if ($form->is_global) {
            $form = Form::firstOrCreate([
                'franchisee_id' => $auth->franchisee_id,
                'parent_form_id' => $form->id,
            ], [
                'is_global' => false,
                'form_setting' => $form->form_setting,
                'is_required_for_translation' => $form->is_required_for_translation,
                'order_no' => $form->order_no,
            ]);
        }

        try {
            if ($mode === 'latest') {
                $this->formTranslationSyncService->updateToLatestGlobal($form, $language);
            } elseif ($mode === 'reset') {
                $this->formTranslationSyncService->resetToGlobal($form, $language);
            } else {
                $this->formTranslationSyncService->adoptGlobal($form, $language);
            }

            $this->formNotificationTranslationSyncService->syncAllFromGlobal($form, $language);

            $form->refresh();
            $this->copyGlobalFormRowSettingsOntoFranchiseForm($form);
        } catch (\RuntimeException $e) {
            return ApiResponse::error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        CacheHelper::forgetByTag('forms');

        $form->refresh()->load(['translations', 'notifications.translations', 'parentForm.translations', 'parentForm.notifications.translations']);

        return ApiResponse::success($this->projectForm($form, $language, $auth->franchisee_id), 'Form synced from global successfully');
    }

    private function upsertNotifications(Form $form, array $notificationsData, string $language, bool $isGlobal): void
    {
        $rows = $this->normalizeNotificationsPayload($notificationsData);
        $globalForm = $isGlobal
            ? $form
            : $form->parentForm()->with('notifications')->first();
        $globalByEvent = collect($globalForm?->notifications ?? [])->keyBy('event_key');

        $existingList = $form->notifications()->get();
        $existingById = $existingList->keyBy('id');
        $existingByEvent = $existingList->keyBy('event_key');
        $existingBySource = $existingList
            ->filter(fn(FormNotification $notification) => !empty($notification->source_global_notification_id))
            ->keyBy('source_global_notification_id');
        $keptIds = [];

        foreach ($rows as $row) {
            /** @var FormNotification $notification */
            $notification = null;
            $sourceGlobalNotificationId = null;
            if (!$isGlobal) {
                $sourceGlobalNotificationId = $row['source_global_notification_id'] ?? null;
                if (!$sourceGlobalNotificationId && !empty($row['event_key'])) {
                    $sourceGlobalNotificationId = $globalByEvent->get($row['event_key'])?->id;
                }
            }

            if (!empty($row['id']) && $existingById->has($row['id'])) {
                $notification = $existingById->get($row['id']);
            } elseif (!empty($sourceGlobalNotificationId) && $existingBySource->has($sourceGlobalNotificationId)) {
                $notification = $existingBySource->get($sourceGlobalNotificationId);
            } elseif (!empty($row['event_key']) && $existingByEvent->has($row['event_key'])) {
                $notification = $existingByEvent->get($row['event_key']);
            }

            if (!$notification) {
                $notification = new FormNotification();
                $notification->form_id = $form->id;
            }

            $baseChanged = !$notification->exists
                || $notification->notification_name != ($row['notification_name'] ?? null)
                || $notification->to_email != ($row['to_email'] ?? null)
                || $notification->from_name != ($row['from_name'] ?? null)
                || $notification->from_email != ($row['from_email'] ?? null)
                || $notification->cc != ($row['cc'] ?? null)
                || $notification->bcc != ($row['bcc'] ?? null)
                || $notification->event_key != ($row['event_key'] ?? null)
                || $notification->source_global_notification_id != ($isGlobal ? null : $sourceGlobalNotificationId);

            $notification->notification_name = $row['notification_name'];
            $notification->to_email = $row['to_email'] ?? null;
            $notification->from_name = $row['from_name'] ?? null;
            // form_notifications.from_email is NOT NULL in the schema, so never
            // write null (which throws) — coalesce a missing value to empty string.
            $notification->from_email = $row['from_email'] ?? '';
            $notification->cc = $row['cc'] ?? null;
            $notification->bcc = $row['bcc'] ?? null;
            $notification->event_key = $row['event_key'];
            $notification->source_global_notification_id = $isGlobal ? null : $sourceGlobalNotificationId;
            $notification->save();

            if ($isGlobal && $baseChanged) {
                $this->formNotificationTranslationSyncService->touchGlobalNotificationVersions($notification);
            }

            $keptIds[] = $notification->id;

            $translationPayload = [
                'subject' => $row['subject'] ?? null,
                'message' => $row['message'] ?? null,
                'status' => $row['status'] ?? 'Draft',
            ];

            if ($isGlobal) {
                $this->formNotificationTranslationSyncService->saveGlobalTranslation($notification, $language, $translationPayload);
            } else {
                $this->formNotificationTranslationSyncService->saveFranchiseManualTranslation($notification, $language, $translationPayload);
            }
        }

        $toDelete = $existingById->keys()->diff($keptIds)->values();
        if ($toDelete->isNotEmpty()) {
            FormNotificationTranslation::whereIn('form_notification_id', $toDelete)->delete();
            FormNotification::whereIn('id', $toDelete)->delete();
        }
    }

    private function normalizeNotificationsPayload(array $notificationsData): array
    {
        $rows = [];
        $seen = [];

        foreach ($notificationsData as $row) {
            if (!is_array($row)) {
                continue;
            }

            $eventKey = trim((string) ($row['event_key'] ?? ''));
            if ($eventKey === '') {
                continue;
            }

            $identity = (string) ($row['id'] ?? '');
            if ($identity === '') {
                $identity = 'event:' . strtolower($eventKey);
            }

            if (isset($seen[$identity])) {
                $rows[$seen[$identity]] = array_merge($rows[$seen[$identity]], $row);
                continue;
            }

            $seen[$identity] = count($rows);
            $rows[] = $row;
        }

        return array_values($rows);
    }

    private function resolveAccessibleBaseForms($auth): Collection
    {
        if ($auth->role === 'super_admin') {
            return Form::query()
                ->whereNull('franchisee_id')
                ->where('is_global', true)
                ->orderBy('order_no')
                ->get();
        }

        $franchiseOverrides = Form::query()
            ->where('franchisee_id', $auth->franchisee_id)
            ->whereNull('deleted_at')
            ->get();

        $overriddenParentIds = $franchiseOverrides->pluck('parent_form_id')->filter()->all();

        $globals = Form::query()
            ->whereNull('franchisee_id')
            ->where('is_global', true)
            ->whereNull('deleted_at')
            ->whereNotIn('id', $overriddenParentIds)
            ->get();

        return $franchiseOverrides->merge($globals)->values();
    }

    private function resolveAvailableLanguages(Form $form): array
    {
        $languages = collect($form->translations)->pluck('language');

        if ($form->parentForm) {
            $languages = $languages->merge(collect($form->parentForm->translations)->pluck('language'));
        }

        return $languages->filter()->map(fn($x) => strtolower((string) $x))->unique()->sort()->values()->all();
    }

    private function projectForm(Form $form, string $language, ?string $viewerFranchiseeId = null, ?array $availableLanguages = null): array
    {
        $language = strtolower($language ?: 'en');
        $availableLanguages = $availableLanguages ?? $this->resolveAvailableLanguages($form);

        $globalForm = $this->resolveGlobalForm($form);
        $globalTranslations = collect($globalForm?->translations ?? []);
        $localTranslations = $viewerFranchiseeId
            ? ($form->franchisee_id === $viewerFranchiseeId ? collect($form->translations ?? []) : collect())
            : collect($form->translations ?? []);

        if ($viewerFranchiseeId) {
            $franchiseRequested = $localTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === $language);
            $franchiseEnglish = $localTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === 'en');
            $globalRequested = $globalTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === $language);
            $globalEnglish = $globalTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === 'en');

            if (
                $globalRequested
                && $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($language) !== $language
            ) {
                $globalRequested = null;
            }

            $displayTranslation = $franchiseRequested;
            $fallbackSource = 'franchise_preferred';

            if (!$displayTranslation) {
                $displayTranslation = $globalRequested;
                $fallbackSource = 'global_preferred';
            }
            if (!$displayTranslation) {
                $displayTranslation = $franchiseEnglish;
                $fallbackSource = 'franchise_en';
            }
            if (!$displayTranslation) {
                $displayTranslation = $globalEnglish;
                $fallbackSource = 'global_en';
            }

            $franchiseSourceTranslation = $franchiseRequested ?: $franchiseEnglish;
            $globalSourceTranslation = $globalRequested ?: $globalEnglish;
            $usingFallback = $fallbackSource !== 'franchise_preferred';
        } else {
            $franchiseSourceTranslation = $localTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === $language)
                ?: $localTranslations->first(fn(FormTranslation $t) => strtolower((string) $t->language) === 'en');
            $globalSourceTranslation = null;
            $displayTranslation = $franchiseSourceTranslation;
            $fallbackSource = $displayTranslation ? 'global_preferred' : null;
            $usingFallback = $displayTranslation && strtolower((string) $displayTranslation->language) !== $language;
        }

        $effectiveLanguage = strtolower((string) ($displayTranslation?->language ?: $language));
        $syncState = $viewerFranchiseeId
            ? $this->formTranslationSyncService->syncState($franchiseSourceTranslation, $globalSourceTranslation)
            : FormTranslationSyncService::STATE_NOT_ADOPTED;
        $formDiff = [];
        if ($viewerFranchiseeId && $franchiseSourceTranslation && $globalSourceTranslation) {
            $formDiff = $this->meaningfulContentDiffService->filter(
                $this->formTranslationDiffService->diff($franchiseSourceTranslation, $globalSourceTranslation)
            );
        }
        $formMeaningful = count($formDiff) > 0;
        $formVersionOutdated = $viewerFranchiseeId
            && $franchiseSourceTranslation
            && $globalSourceTranslation
            && $this->formTranslationSyncService->hasUpdateAvailable($franchiseSourceTranslation, $globalSourceTranslation);
        $formUpdateAvailable = $formVersionOutdated && $formMeaningful;

        $notificationProjection = $this->projectNotifications(
            $form,
            $language,
            $globalForm,
            $viewerFranchiseeId
        );

        $notificationUpdateAvailable = (bool) ($notificationProjection['notification_update_available'] ?? false);
        $notificationCanResetPart = (bool) ($notificationProjection['notification_can_reset_to_global'] ?? false);
        $updateAvailable = $formUpdateAvailable || $notificationUpdateAvailable;

        $formGlobalPublished = $globalSourceTranslation && $this->isPublishedFormContentStatus($globalSourceTranslation->status);
        $formResetPart = (bool) (
            $viewerFranchiseeId
            && $franchiseSourceTranslation
            && $globalSourceTranslation
            && $formGlobalPublished
            && !$formUpdateAvailable
            && $formMeaningful
        );

        $canResetToGlobal = (bool) (
            $viewerFranchiseeId
            && !$updateAvailable
            && ($formResetPart || $notificationCanResetPart)
        );

        $sourceGlobalDeleted = !is_null($form->parent_form_id) && is_null($form->parentForm);

        return [
            'id' => $form->id,
            'franchisee_id' => $form->franchisee_id,
            'parent_form_id' => $form->parent_form_id,
            'is_global' => (bool) $form->is_global,
            'form_setting' => $displayTranslation?->form_setting ?? $form->form_setting,
            'is_required_for_translation' => (bool) $form->is_required_for_translation,
            'order_no' => $form->order_no,
            'translation_id' => $displayTranslation?->id,
            'language' => $language,
            'displayed_language' => $effectiveLanguage,
            'name' => $displayTranslation?->name,
            'description' => $displayTranslation?->description,
            'fields' => $displayTranslation?->fields,
            'pages' => $displayTranslation?->pages,
            'status' => $displayTranslation?->status,
            'translated' => (bool) ($displayTranslation?->translated ?? false),
            'created_at' => $form->created_at,
            'updated_at' => $form->updated_at,
            'available_languages' => $availableLanguages,
            'sync_state' => $syncState,
            'adopted_from_global' => (bool) ($franchiseSourceTranslation?->adopted_from_global ?? false),
            'source_global_translation_id' => $franchiseSourceTranslation?->source_global_translation_id,
            'source_global_version' => $franchiseSourceTranslation?->source_global_version,
            'current_global_version' => $globalSourceTranslation?->version,
            'form_update_available' => $formUpdateAvailable,
            'notification_update_available' => $notificationUpdateAvailable,
            'update_available' => $updateAvailable,
            'can_reset_to_global' => $canResetToGlobal,
            'show_reset_to_global' => $canResetToGlobal,
            'show_update_to_latest_global' => $updateAvailable,
            'effective_language' => $effectiveLanguage,
            'using_fallback' => $usingFallback,
            'fallback_source' => $fallbackSource,
            'latest_global_form_version' => $globalSourceTranslation?->version,
            'franchise_source_form_version' => $franchiseSourceTranslation?->source_global_version,
            'latest_global_notification_version' => $notificationProjection['latest_global_notification_version'],
            'franchise_source_notification_version' => $notificationProjection['franchise_source_notification_version'],
            'global_translation_id' => $globalSourceTranslation?->id,
            'franchise_translation_id' => $franchiseSourceTranslation?->id,
            'source_global_deleted' => $sourceGlobalDeleted,
            'source_global_deleted_message' => $sourceGlobalDeleted ? self::GLOBAL_DELETED_MESSAGE : null,
            'notifications' => $notificationProjection['notifications'],
        ];
    }

    private function projectNotifications(
        Form $form,
        string $language,
        ?Form $globalForm,
        ?string $viewerFranchiseeId = null
    ): array
    {
        $localByEvent = $viewerFranchiseeId
            ? (($form->franchisee_id === $viewerFranchiseeId)
                ? collect($form->notifications ?? [])->keyBy('event_key')
                : collect())
            : collect($form->notifications ?? [])->keyBy('event_key');
        $globalByEvent = collect($globalForm?->notifications ?? [])->keyBy('event_key');
        $eventKeys = $viewerFranchiseeId
            ? $localByEvent->keys()->merge($globalByEvent->keys())->unique()->values()
            : $localByEvent->keys()->values();

        $rows = [];
        $notificationUpdateAvailable = false;
        $notificationCanResetPart = false;
        $latestGlobalVersion = null;
        $franchiseSourceVersion = null;

        foreach ($eventKeys as $eventKey) {
            $localNotification = $localByEvent->get($eventKey);
            $globalNotification = $globalByEvent->get($eventKey);
            $notification = $localNotification ?: $globalNotification;

            if (!$notification instanceof FormNotification) {
                continue;
            }

            $localTranslations = collect($localNotification?->translations ?? []);
            $globalTranslations = collect($globalNotification?->translations ?? []);

            $localRequested = $localTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === $language);
            $localEnglish = $localTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === 'en');
            $globalRequested = $globalTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === $language);
            $globalEnglish = $globalTranslations->first(fn(FormNotificationTranslation $t) => strtolower((string) $t->language) === 'en');

            if (
                $viewerFranchiseeId
                && $globalRequested
                && $this->franchiseGlobalSourceResolver->effectiveGlobalSourceLanguage($language) !== $language
            ) {
                $globalRequested = null;
            }

            $displayTranslation = $localRequested ?: $globalRequested ?: $localEnglish ?: $globalEnglish;
            $franchiseSourceTranslation = $localRequested ?: $localEnglish;
            $globalSourceTranslation = $globalRequested ?: $globalEnglish;
            $label = $notification->notification_name ?: (string) $eventKey;

            $itemDiff = [];
            if ($viewerFranchiseeId && $franchiseSourceTranslation && $globalSourceTranslation) {
                $itemDiff = $this->formNotificationTranslationDiffService->diff(
                    $franchiseSourceTranslation,
                    $globalSourceTranslation,
                    $label
                );
            }

            $itemMeaningful = $viewerFranchiseeId
                && $franchiseSourceTranslation
                && $globalSourceTranslation
                && $this->meaningfulContentDiffService->hasMeaningfulChanges($itemDiff);

            $itemVersionOutdated = $viewerFranchiseeId
                && $franchiseSourceTranslation
                && $globalSourceTranslation
                && $this->formNotificationTranslationSyncService->hasUpdateAvailable(
                    $franchiseSourceTranslation,
                    $globalSourceTranslation
                );

            $itemUpdateAvailable = $itemVersionOutdated && $itemMeaningful;

            if ($itemUpdateAvailable) {
                $notificationUpdateAvailable = true;
            }

            $itemGlobalPublished = $globalSourceTranslation && $this->isPublishedFormContentStatus($globalSourceTranslation->status);
            if (
                $viewerFranchiseeId
                && $franchiseSourceTranslation
                && $globalSourceTranslation
                && $itemGlobalPublished
                && !$itemUpdateAvailable
                && $itemMeaningful
            ) {
                $notificationCanResetPart = true;
            }

            if ($globalSourceTranslation?->version !== null) {
                $latestGlobalVersion = max((int) ($latestGlobalVersion ?? 0), (int) $globalSourceTranslation->version);
            }
            if ($franchiseSourceTranslation?->source_global_version !== null) {
                $franchiseSourceVersion = max(
                    (int) ($franchiseSourceVersion ?? 0),
                    (int) $franchiseSourceTranslation->source_global_version
                );
            }

            $availableLanguages = $localTranslations->pluck('language')
                ->merge($globalTranslations->pluck('language'))
                ->filter()
                ->map(fn($x) => strtolower((string) $x))
                ->unique()
                ->sort()
                ->values()
                ->all();

            $rows[] = [
                'id' => $notification->id,
                'form_id' => $notification->form_id,
                'notification_name' => $notification->notification_name,
                'to_email' => $notification->to_email,
                'from_name' => $notification->from_name,
                'from_email' => $notification->from_email,
                'cc' => $notification->cc,
                'bcc' => $notification->bcc,
                'event_key' => $notification->event_key,
                'source_global_notification_id' => $notification->source_global_notification_id,
                'translation_id' => $displayTranslation?->id,
                'language' => $language,
                'subject' => $displayTranslation?->subject,
                'message' => $displayTranslation?->message,
                'status' => $displayTranslation?->status,
                'update_available' => $itemUpdateAvailable,
                'translation_family' => collect($availableLanguages)->map(function ($lang) use ($localTranslations, $globalTranslations) {
                    $local = $localTranslations->firstWhere('language', $lang);
                    $global = $globalTranslations->firstWhere('language', $lang)
                        ?: $globalTranslations->firstWhere('language', 'en');
                    $display = $local ?: $global;

                    return [
                        'language' => $lang,
                        'translation_id' => $display?->id,
                        'subject' => $display?->subject,
                        'message' => $display?->message,
                        'status' => $display?->status,
                    ];
                })->values()->all(),
                'available_languages' => $availableLanguages,
                'adopted_from_global' => (bool) ($franchiseSourceTranslation?->adopted_from_global ?? false),
                'source_global_translation_id' => $franchiseSourceTranslation?->source_global_translation_id,
                'source_global_version' => $franchiseSourceTranslation?->source_global_version,
            ];
        }

        return [
            'notifications' => array_values($rows),
            'notification_update_available' => $notificationUpdateAvailable,
            'notification_can_reset_to_global' => $notificationCanResetPart,
            'latest_global_notification_version' => $latestGlobalVersion,
            'franchise_source_notification_version' => $franchiseSourceVersion,
        ];
    }

    private function resolveGlobalForm(Form $form): ?Form
    {
        if ($form->is_global && is_null($form->parent_form_id)) {
            return $form;
        }

        return $form->parentForm;
    }

    /**
     * Copy parent global row settings onto the franchise override after adopt/reset/latest sync.
     */
    private function copyGlobalFormRowSettingsOntoFranchiseForm(Form $form): void
    {
        $parentId = $form->parent_form_id;
        if (!$parentId) {
            return;
        }

        $global = Form::withTrashed()->find($parentId);
        if (!$global) {
            return;
        }

        $form->form_setting = $global->form_setting;
        $form->is_required_for_translation = (bool) $global->is_required_for_translation;
        $form->save();
    }
}
