<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\REST;

defined('ABSPATH') || exit;

use CliqThemes\StoreLocator\Geocoding\GeocodingService;
use CliqThemes\StoreLocator\I18n\Multilingual;
use CliqThemes\StoreLocator\Repository\BrandRepository;
use CliqThemes\StoreLocator\Repository\CategoryRepository;
use CliqThemes\StoreLocator\Repository\CustomFieldsRepository;
use CliqThemes\StoreLocator\Repository\StoreRepository;
use CliqThemes\StoreLocator\Support\SeoSchemaService;
use CliqThemes\StoreLocator\Support\SettingsService;
use WP_REST_Request;
use WP_REST_Response;

/**
 * Authenticated admin REST surface (capability: manage_store_locator) backing
 * the React management UI. Namespace: cliq-store-locator/v1, all routes under
 * /admin/*. Handlers are thin and delegate to repositories/services.
 */
final class AdminController
{
    use RespondsToRest;

    public const NAMESPACE = 'cliq-store-locator/v1';

    public function register(): void
    {
        add_action('rest_api_init', [$this, 'registerRoutes']);
    }

    public function permission(): bool
    {
        return current_user_can('manage_store_locator') || current_user_can('manage_options');
    }

    public function registerRoutes(): void
    {
        $perm = [$this, 'permission'];

        // Stores
        $this->route('/admin/stores', 'GET', [$this, 'listStores'], $perm);
        $this->route('/admin/stores', 'POST', [$this, 'createStore'], $perm);
        $this->route('/admin/stores/bulk', 'POST', [$this, 'bulkStores'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)', 'GET', [$this, 'getStore'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)', 'PUT', [$this, 'updateStore'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)', 'DELETE', [$this, 'deleteStore'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)/duplicate', 'POST', [$this, 'duplicateStore'], $perm);

        // Taxonomy-style entities
        $this->crud('categories', new CategoryRepository(), $perm, true);
        $this->crud('brands', new BrandRepository(), $perm);
        $this->crud('custom-fields', new CustomFieldsRepository(), $perm);

        /**
         * Register admin routes for a feature of your own.
         *
         * @param callable $permission the capability callback these routes use
         */
        do_action('cliq_sl_register_admin_routes', $perm);

        // Settings is a slot too, at one URL: the default owner writes only the
        // keys this plugin defines, and a claimant accepts the whole surface.
        if (apply_filters('cliq_sl_admin_settings_owner', 'allowlist') === 'full') {
            $this->route('/admin/settings', 'GET', [$this, 'getSettings'], $perm);
            $this->route('/admin/settings', 'POST', [$this, 'updateSettings'], $perm);
        }

        // Tools
        $this->route('/admin/geocode', 'POST', [$this, 'geocode'], $perm);
        $this->route('/admin/geocode/fill-missing', 'POST', [$this, 'fillMissing'], $perm);
        // The dashboard overview is a slot: a feature with a richer answer
        // claims it, otherwise the counts below answer. Ownership is asked
        // about, because registering both would leave it to hook order.
        if (apply_filters('cliq_sl_owns_admin_overview', 'core') === 'core') {
            $this->route('/admin/overview', 'GET', [$this, 'overview'], $perm);
        }
        $this->route('/admin/contact', 'POST', [$this, 'contact'], $perm);

        // SEO preview
        $this->route('/admin/seo/(?P<id>\d+)', 'GET', [$this, 'seo'], $perm);

        // Multilingual (TRID translations)
        $this->route('/admin/languages', 'GET', [$this, 'languages'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)/translations', 'GET', [$this, 'getStoreTranslations'], $perm);
        $this->route('/admin/stores/(?P<id>\d+)/translations/(?P<lang>[A-Za-z_\-]+)', 'PUT', [$this, 'saveStoreTranslation'], $perm);
        $this->route('/admin/categories/(?P<id>\d+)/translations', 'GET', [$this, 'getCategoryTranslations'], $perm);
        $this->route('/admin/categories/(?P<id>\d+)/translations/(?P<lang>[A-Za-z_\-]+)', 'PUT', [$this, 'saveCategoryTranslation'], $perm);
        $this->route('/admin/brands/(?P<id>\d+)/translations', 'GET', [$this, 'getBrandTranslations'], $perm);
        $this->route('/admin/brands/(?P<id>\d+)/translations/(?P<lang>[A-Za-z_\-]+)', 'PUT', [$this, 'saveBrandTranslation'], $perm);

        // Onboarding / demo setup
        $this->route('/admin/onboarding', 'GET', [$this, 'onboardingStatus'], $perm);
        $this->route('/admin/onboarding/seed', 'POST', [$this, 'onboardingSeed'], $perm);
        $this->route('/admin/onboarding/dismiss', 'POST', [$this, 'onboardingDismiss'], $perm);
    }

    /** Onboarding state for the setup wizard: done flag, store count, available demos. */
    public function onboardingStatus(): WP_REST_Response
    {
        return $this->ok([
            'onboarded' => (bool) get_option('cliq_sl_onboarded', false),
            'storeCount' => (int) \CliqThemes\StoreLocator\Models\Store::query()->whereNull('source_id')->count(),
            'verticals' => \CliqThemes\StoreLocator\Support\DemoData::verticals(),
        ]);
    }

    /** Seed a chosen demo vertical (creates its category + 20 stores) and mark onboarded. */
    public function onboardingSeed(WP_REST_Request $request): WP_REST_Response
    {
        $vertical = sanitize_key((string) $request->get_param('vertical'));
        if ($vertical === '') {
            return $this->ok(['message' => __('No demo selected.', 'cliq-store-locator')], 400);
        }
        $result = \CliqThemes\StoreLocator\Support\DemoData::seed($vertical);
        update_option('cliq_sl_onboarded', true);

        return $this->ok($result);
    }

    /** Skip the wizard without seeding. */
    public function onboardingDismiss(): WP_REST_Response
    {
        update_option('cliq_sl_onboarded', true);

        return $this->ok(['onboarded' => true]);
    }

    /** Active languages for the admin language switcher (empty when single-language). */
    public function languages(): WP_REST_Response
    {
        return $this->ok([
            'active' => Multilingual::isActive(),
            'languages' => Multilingual::languages(),
        ]);
    }

    public function getStoreTranslations(WP_REST_Request $request): WP_REST_Response
    {
        return $this->ok(['translations' => (new StoreRepository())->translations((int) $request['id'])]);
    }

    public function saveStoreTranslation(WP_REST_Request $request): WP_REST_Response
    {
        $result = (new StoreRepository())->saveTranslation((int) $request['id'], (string) $request['lang'], $this->body($request));

        return $result ? $this->ok($result) : $this->notFound();
    }

    public function getCategoryTranslations(WP_REST_Request $request): WP_REST_Response
    {
        return $this->ok(['translations' => (new CategoryRepository())->translations((int) $request['id'])]);
    }

    public function saveCategoryTranslation(WP_REST_Request $request): WP_REST_Response
    {
        $result = (new CategoryRepository())->saveTranslation((int) $request['id'], (string) $request['lang'], $this->body($request));

        return $result ? $this->ok($result) : $this->notFound();
    }

    public function getBrandTranslations(WP_REST_Request $request): WP_REST_Response
    {
        return $this->ok(['translations' => (new BrandRepository())->translations((int) $request['id'])]);
    }

    public function saveBrandTranslation(WP_REST_Request $request): WP_REST_Response
    {
        $result = (new BrandRepository())->saveTranslation((int) $request['id'], (string) $request['lang'], $this->body($request));

        return $result ? $this->ok($result) : $this->notFound();
    }

    public function seo(WP_REST_Request $request): WP_REST_Response
    {
        $schema = (new SeoSchemaService())->forStore((int) $request['id']);

        return $schema ? $this->ok($schema) : $this->notFound();
    }

    // ---- Stores ----------------------------------------------------------

    public function listStores(WP_REST_Request $request): WP_REST_Response
    {
        $this->applyAdminLanguage($request);

        return $this->ok((new StoreRepository())->list($request->get_params()));
    }

    public function getStore(WP_REST_Request $request): WP_REST_Response
    {
        $store = (new StoreRepository())->find((int) $request['id']);

        return $store ? $this->ok($store) : $this->notFound();
    }

    public function createStore(WP_REST_Request $request): WP_REST_Response
    {
        try {
            return $this->ok((new StoreRepository())->create($this->body($request)), 201);
        } catch (\Throwable $e) {
            return $this->writeFailed($e);
        }
    }

    public function updateStore(WP_REST_Request $request): WP_REST_Response
    {
        try {
            $store = (new StoreRepository())->update((int) $request['id'], $this->body($request));
        } catch (\Throwable $e) {
            return $this->writeFailed($e);
        }

        return $store ? $this->ok($store) : $this->notFound();
    }

    public function deleteStore(WP_REST_Request $request): WP_REST_Response
    {
        return $this->ok(['deleted' => (new StoreRepository())->delete((int) $request['id'])]);
    }

    public function duplicateStore(WP_REST_Request $request): WP_REST_Response
    {
        $store = (new StoreRepository())->duplicate((int) $request['id']);

        return $store ? $this->ok($store, 201) : $this->notFound();
    }

    public function bulkStores(WP_REST_Request $request): WP_REST_Response
    {
        $body = $this->body($request);
        $ids = array_map('intval', (array) ($body['ids'] ?? []));
        $action = (string) ($body['action'] ?? '');
        $repo = new StoreRepository();

        $affected = match ($action) {
            'delete' => $repo->bulkDelete($ids),
            'status' => $repo->bulkStatus($ids, (string) ($body['status'] ?? 'draft')),
            default => 0,
        };

        return $this->ok(['affected' => $affected]);
    }

    // ---- Leads -----------------------------------------------------------




    public function overview(): WP_REST_Response
    {
        $stores = new StoreRepository();
        $overview = [
            'stores' => $stores->list(['per_page' => 1])['total'],
            'categories' => count((new CategoryRepository())->all()),
        ];
        /**
         * Filter the dashboard overview.
         *
         * @param array<string, mixed> $overview
         */
        $overview = apply_filters('cliq_sl_admin_overview', $overview);

        return $this->ok(is_array($overview) ? $overview : []);
    }

    /**
     * Dashboard "Need a hand?" contact form → emails the CliqThemes support inbox.
     * Email-only (nothing stored). Only reachable by admins (route permission).
     */
    public function contact(WP_REST_Request $request): WP_REST_Response
    {
        $body = $this->body($request);
        $name = sanitize_text_field((string) ($body['name'] ?? ''));
        $email = sanitize_email((string) ($body['email'] ?? ''));
        $message = sanitize_textarea_field((string) ($body['message'] ?? ''));
        $section = sanitize_key((string) ($body['section'] ?? 'support'));
        $siteUrl = esc_url_raw((string) ($body['site_url'] ?? (function_exists('home_url') ? home_url() : '')));

        if ($name === '' || ! is_email($email) || $message === '') {
            return $this->error(__('Please add your name, a valid email and a message.', 'cliq-store-locator'), 422);
        }

        $sections = [
            'billing' => __('Billing', 'cliq-store-locator'),
            'support' => __('Support', 'cliq-store-locator'),
            'consultation' => __('Consultation', 'cliq-store-locator'),
            'custom' => __('Custom work', 'cliq-store-locator'),
        ];
        $sectionLabel = $sections[$section] ?? $sections['support'];

        $subject = sprintf('[Cliq Store Locator] %s enquiry from %s', $sectionLabel, $name);
        $lines = [
            'Section: ' . $sectionLabel,
            'Name:    ' . $name,
            'Email:   ' . $email,
            'Site:    ' . $siteUrl,
            '',
            $message,
        ];
        $headers = [
            'Content-Type: text/plain; charset=UTF-8',
            'Reply-To: ' . $name . ' <' . $email . '>',
        ];

        $sent = function_exists('wp_mail')
            && wp_mail('support@cliqthemes.com', $subject, implode("\n", $lines), $headers);

        if (! $sent) {
            return $this->error(
                __('We couldn’t send your message right now — please email support@cliqthemes.com directly.', 'cliq-store-locator'),
                502
            );
        }

        return $this->ok(['sent' => true]);
    }

    // ---- Tools -----------------------------------------------------------

    public function geocode(WP_REST_Request $request): WP_REST_Response
    {
        $body = $this->body($request);
        $service = new GeocodingService();

        if (! empty($body['address'])) {
            $result = $service->geocode((string) $body['address']);
        } elseif (isset($body['lat'], $body['lng'])) {
            $result = $service->reverse((float) $body['lat'], (float) $body['lng']);
        } else {
            return $this->error(__('Provide an address or lat/lng.', 'cliq-store-locator'), 422);
        }

        return $result ? $this->ok($result->toArray()) : $this->error(__('No result.', 'cliq-store-locator'), 404);
    }

    public function fillMissing(WP_REST_Request $request): WP_REST_Response
    {
        $limit = max(1, min(200, (int) ($this->body($request)['limit'] ?? 50)));

        return $this->ok((new GeocodingService())->fillMissingCoordinates($limit));
    }

    // ---- Generic CRUD wiring --------------------------------------------

    /**
     * Register list/create/get/update/delete for a repository exposing the
     * standard methods. $repo must implement all()/find()/create()/update()/delete().
     */
    private function crud(string $slug, object $repo, callable $perm, bool $withTree = false): void
    {
        $base = '/admin/' . $slug;

        $this->route($base, 'GET', function (WP_REST_Request $request) use ($repo, $withTree): WP_REST_Response {
            $this->applyAdminLanguage($request);
            if ($withTree && $request->get_param('tree') && method_exists($repo, 'tree')) {
                return $this->ok(['items' => $repo->tree()]);
            }

            return $this->ok(['items' => $repo->all()]);
        }, $perm);

        $this->route($base, 'POST', fn (WP_REST_Request $r): WP_REST_Response => $this->ok($repo->create($this->body($r)), 201), $perm);

        $this->route($base . '/(?P<id>\d+)', 'GET', function (WP_REST_Request $r) use ($repo): WP_REST_Response {
            $item = $repo->find((int) $r['id']);

            return $item ? $this->ok($item) : $this->notFound();
        }, $perm);

        $this->route($base . '/(?P<id>\d+)', 'PUT', function (WP_REST_Request $r) use ($repo): WP_REST_Response {
            $item = $repo->update((int) $r['id'], $this->body($r));

            return $item ? $this->ok($item) : $this->notFound();
        }, $perm);

        $this->route($base . '/(?P<id>\d+)', 'DELETE', fn (WP_REST_Request $r): WP_REST_Response => $this->ok(['deleted' => $repo->delete((int) $r['id'])]), $perm);
    }

    /**
     * Apply the admin language switcher's selection so list endpoints overlay
     * the chosen language's translated text. Empty/absent = default (originals).
     */
    private function applyAdminLanguage(WP_REST_Request $request): void
    {
        $lang = $request->get_param('lang');
        if ($lang !== null) {
            Multilingual::setOverride((string) $lang);
        }
    }

    private function route(string $path, string $methods, callable $callback, callable $permission): void
    {
        register_rest_route(self::NAMESPACE, $path, [
            'methods' => $methods,
            'callback' => $callback,
            'permission_callback' => $permission,
        ]);
    }




    private function error(string $message, int $status): WP_REST_Response
    {
        return new WP_REST_Response(['message' => $message], $status);
    }

    /**
     * Convert a write-path exception (e.g. a rolled-back transaction) into a
     * 500 response. The detail is logged, not leaked to the client.
     */
    private function writeFailed(\Throwable $e): WP_REST_Response
    {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Diagnostic is explicitly gated by WP_DEBUG.
            error_log('[cliq-store-locator] store write failed: ' . $e->getMessage());
        }

        return $this->error(__('The record could not be saved. Please try again.', 'cliq-store-locator'), 500);
    }
}
