<?php

namespace App\Repositories;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Mtc\MercuryDataModels\Setting;

class SiteExportRepository
{
    /**
     * Suffix of the PII-redacted views created by mercury-data-models
     * (2026_06_17_120000_create_pii_redacted_views).
     */
    public const REDACTED_VIEW_SUFFIX = '_redacted';

    /**
     * Tables holding personal data. These are only ever served through their
     * `<table>_redacted` view. If the view does not exist on the tenant the
     * table is excluded from the export entirely — raw rows never leave.
     */
    public const REDACTED_TABLES = [
        'reservations',
        'enquiries',
        'enquiry_answers',
        'customers',
        'customer_notes',
        'invoices',
        'invoice_addresses',
    ];

    /**
     * Exportable tables grouped into modules a partial sync can select from.
     * Anything not listed here can never be exported.
     */
    public const MODULES = [
        'content' => [
            'label' => 'Content (pages, templates, elements, globals, menus, SEO)',
            'tables' => [
                'pages',
                'page_categories',
                'page_content',
                'page_tags',
                'page_templates',
                'page_template_elements',
                'page_versions',
                'version_content',
                'content_categories',
                'content_elements',
                'content_element_fields',
                'content_tags',
                'global_content',
                'menus',
                'menu_entries',
                'seo_data',
                'seo_defaults',
                'seo_redirects',
                'shortcodes',
                'shortcode_conditions',
                'replacements',
            ],
        ],
        'media' => [
            'label' => 'Media library (records referenced by most other modules)',
            'tables' => [
                'media',
                'media_folders',
                'media_sizes',
                'media_tags',
                'media_uses',
            ],
        ],
        'settings' => [
            'label' => 'Settings & properties',
            'tables' => [
                'settings',
                'properties',
                'property_attribute_values',
                'property_categories',
                'property_category_fields',
            ],
        ],
        'dealerships' => [
            'label' => 'Dealerships & franchises',
            'tables' => [
                'dealerships',
                'dealership_content',
                'dealership_departments',
                'dealership_franchises',
                'dealership_holidays',
                'franchises',
            ],
        ],
        'vehicles' => [
            'label' => 'Vehicle data (stock, finance, tech data, taxonomies, labels)',
            'tables' => [
                'vehicles',
                'vehicle_attributes',
                'vehicle_attribute_values',
                'vehicle_autotrader_data',
                'vehicle_autotrader_derivative_mappings',
                'vehicle_features',
                'vehicle_finance',
                'vehicle_labels',
                'vehicle_price_history',
                'vehicle_standard_equipment',
                'vehicle_tech_data',
                'vehicle_trims',
                'vehicle_trim_content',
                'vehicle_views',
                'vehicle_makes',
                'vehicle_models',
                'body_styles',
                'colours',
                'colour_mapping',
                'drivetrains',
                'fuel_types',
                'transmissions',
                'filter_features',
                'labels',
                'label_rules',
                'lease_vehicle_labels',
            ],
        ],
        'offers' => [
            'label' => 'New cars & offers',
            'tables' => [
                'new_cars',
                'new_car_content',
                'new_car_content_history',
                'new_car_finance',
                'new_car_fuel_types',
                'vehicle_offers',
                'vehicle_offer_content',
                'vehicle_offer_content_history',
                'vehicle_offer_finance',
                'vehicle_offer_fuel_types',
                'vehicle_offer_labels',
                'offer_types',
                'offer_finance_field_configs',
                'offer_views',
                'catalog_offers',
                'catalog_offer_rules',
                'catalog_offer_vehicle',
            ],
        ],
        'car-config' => [
            'label' => 'Car configurator',
            'tables' => [
                'car_configurator_colours',
                'car_configurator_custom_sections',
                'car_configurator_editions',
                'car_configurator_engines',
                'car_configurator_extras',
                'car_configurator_interiors',
                'car_configurator_models',
                'car_configurator_model_extras',
                'car_configurator_model_packages',
                'car_configurator_packages',
                'car_configurator_restrictions',
                'car_configurator_sections',
                'car_configurator_trims',
                'car_configurator_wheels',
            ],
        ],
        'forms' => [
            'label' => 'Forms',
            'tables' => [
                'forms',
                'form_actions',
                'form_dealership',
                'form_questions',
                'form_sections',
            ],
        ],
        'customers' => [
            'label' => 'Customer data — enquiries, reservations, invoices (PII redacted)',
            'tables' => [
                'customers',
                'customer_notes',
                'enquiries',
                'enquiry_answers',
                'enquiry_statuses',
                'enquiry_types',
                'reservations',
                'reservation_prices',
                'reservation_price_rules',
                'reservation_statuses',
                'invoices',
                'invoice_addresses',
            ],
        ],
    ];

    /**
     * All module keys, i.e. what a full sync selects.
     *
     * @return string[]
     */
    public function moduleKeys(): array
    {
        return array_keys(self::MODULES);
    }

    /**
     * Resolve module keys to the tables they cover. Unknown keys are ignored,
     * an empty selection means every module (full sync).
     *
     * @return string[]
     */
    public function tablesForModules(array $modules): array
    {
        $modules = array_intersect($modules ?: $this->moduleKeys(), $this->moduleKeys());

        $tables = [];
        foreach ($modules as $module) {
            $tables = array_merge($tables, self::MODULES[$module]['tables']);
        }

        return array_values(array_unique($tables));
    }

    /**
     * Get the table list for the selected modules with record counts.
     * Tables missing on the tenant are skipped, as are PII tables whose
     * redacted view is missing.
     */
    public function getTablesWithCounts(array $modules): array
    {
        $tableData = [];
        foreach ($this->tablesForModules($modules) as $table) {
            if (!Schema::hasTable($table)) {
                continue;
            }

            $source = $this->exportSource($table);
            if ($source === null) {
                continue;
            }

            $tableData[] = [
                'name' => $table,
                'count' => DB::table($source)->count(),
                'redacted' => $source !== $table,
            ];
        }

        return $tableData;
    }

    /**
     * Check if a table may be exported at all (member of any module).
     */
    public function isTableAllowed(string $table): bool
    {
        return in_array($table, $this->tablesForModules([]), true);
    }

    /**
     * Resolve the relation rows are read from. PII tables resolve to their
     * redacted view, or to null (not exportable) when the view is missing.
     */
    public function exportSource(string $table): ?string
    {
        if (!in_array($table, self::REDACTED_TABLES, true)) {
            return $table;
        }

        $view = $table . self::REDACTED_VIEW_SUFFIX;

        return Schema::hasView($view) ? $view : null;
    }

    /**
     * Get paginated data for a table
     */
    public function getTableData(string $table, int $page = 1, int $perPage = 100): array
    {
        $data = DB::table($this->exportSource($table))
            ->orderBy('id')
            ->paginate($perPage, ['*'], 'page', $page);

        return [
            'data' => $data->items(),
            'pagination' => [
                'current_page' => $data->currentPage(),
                'last_page' => $data->lastPage(),
                'per_page' => $data->perPage(),
                'total' => $data->total(),
            ],
        ];
    }

    /**
     * Get settings with special handling for encrypted secrets
     */
    public function getSettingsData(string $encryptionKey, int $page = 1, int $perPage = 100): array
    {
        $settings = Setting::query()
            ->orderBy('id')
            ->paginate($perPage, ['*'], 'page', $page);

        $data = $settings->map(function (Setting $setting) use ($encryptionKey) {
            $row = $setting->toArray();

            // Re-encrypt secret values using the auth token
            if ($setting->type === 'secret' && $setting->value !== null) {
                $row['value'] = $this->encryptWithToken($setting->value, $encryptionKey);
                $row['encrypted_with_token'] = true;
            }

            return $row;
        });

        return [
            'data' => $data->values()->toArray(),
            'pagination' => [
                'current_page' => $settings->currentPage(),
                'last_page' => $settings->lastPage(),
                'per_page' => $settings->perPage(),
                'total' => $settings->total(),
            ],
        ];
    }

    /**
     * The media disk holding the current tenant's library files. Only this
     * disk is exportable: customer-uploaded attachments (enquiries, invoices)
     * live on the separate file-storage disk and are never served here.
     */
    public function mediaDisk(): string
    {
        return config('filesystems.default_media');
    }

    /**
     * Root prefix under which the current tenant's media files are stored.
     */
    public function mediaPrefix(): string
    {
        return tenant('id') . '/';
    }

    /**
     * List every media file stored for the current tenant with byte sizes and
     * a directly downloadable URL. Paths are already prefixed with the tenant
     * id so they can be mirrored verbatim on the target, matching the exported
     * media table rows. The URL lets the client pull the bytes straight from
     * object storage rather than streaming them through this environment.
     *
     * @return array<int, array{path: string, size: int, url: string|null}>
     */
    public function getMediaFiles(): array
    {
        $disk = Storage::disk($this->mediaDisk());
        $files = [];

        foreach ($disk->getDriver()->listContents($this->mediaPrefix(), true) as $item) {
            if (!$item->isFile()) {
                continue;
            }

            $files[] = [
                'path' => $item->path(),
                'size' => (int) $item->fileSize(),
                'url' => $this->mediaFileUrl($item->path()),
            ];
        }

        return $files;
    }

    /**
     * A directly downloadable URL for a media file. Prefers a short-lived
     * signed URL on object-storage disks, falls back to the public URL, and
     * returns null when the disk exposes neither (the client then streams the
     * bytes through this environment instead).
     */
    public function mediaFileUrl(string $path): ?string
    {
        $disk = Storage::disk($this->mediaDisk());

        try {
            return $disk->temporaryUrl($path, now()->addDay());
        } catch (\Throwable) {
            try {
                return $disk->url($path);
            } catch (\Throwable) {
                return null;
            }
        }
    }

    /**
     * Guard file requests to the current tenant's media directory, blocking
     * any path that would escape it.
     */
    public function isMediaFileAllowed(string $path): bool
    {
        return str_starts_with($path, $this->mediaPrefix())
            && !str_contains($path, '..')
            && !str_contains($path, "\0");
    }

    /**
     * Encrypt a value using the provided token as key
     */
    public function encryptWithToken(string $value, string $token): string
    {
        $key = hash('sha256', $token, true);
        $iv = openssl_random_pseudo_bytes(16);
        $encrypted = openssl_encrypt($value, 'AES-256-CBC', $key, 0, $iv);

        return base64_encode($iv . '::' . $encrypted);
    }

    /**
     * Decrypt a value that was encrypted with the auth token
     */
    public function decryptWithToken(string $encryptedValue, string $token): string
    {
        $key = hash('sha256', $token, true);
        $parts = explode('::', base64_decode($encryptedValue), 2);

        if (count($parts) !== 2) {
            throw new \RuntimeException('Invalid encrypted value format');
        }

        [$iv, $encrypted] = $parts;

        $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);

        if ($decrypted === false) {
            throw new \RuntimeException('Failed to decrypt value');
        }

        return $decrypted;
    }
}
