<?php

namespace App\Services;

use App\Repositories\SiteExportRepository;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Mtc\MercuryDataModels\Setting;

class SiteDownloadService
{
    private string $baseUrl;
    private string $token;
    private string $email;
    private string $tenantId;

    public function __construct(
        protected SiteExportRepository $repository
    ) {
    }

    /**
     * Configure the service with remote connection details
     */
    public function setConfig(string $baseUrl, string $token, string $email, string $tenantId): self
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->token = $token;
        $this->email = $email;
        $this->tenantId = $tenantId;

        return $this;
    }

    /**
     * Test connection to the remote server
     */
    public function testConnection(): array
    {
        try {
            $response = Http::timeout(30)
                ->withHeaders($this->getHeaders())
                ->post($this->getEndpoint('tables'), [
                    'modules' => ['settings'],
                ]);

            if ($response->successful()) {
                return [
                    'success' => true,
                    'message' => 'Connection successful',
                ];
            }

            if ($response->status() === 403) {
                return [
                    'success' => false,
                    'message' => 'Authentication failed. Please check your token and email.',
                ];
            }

            return [
                'success' => false,
                'message' => 'Connection failed: ' . $response->status(),
            ];
        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => 'Connection error: ' . $e->getMessage(),
            ];
        }
    }

    /**
     * Get list of available tables from remote for the selected modules.
     * An empty module list means a full sync.
     */
    public function getTableList(array $modules = []): array
    {
        $response = Http::timeout(60)
            ->withHeaders($this->getHeaders())
            ->post($this->getEndpoint('tables'), [
                'modules' => $modules,
            ]);

        if (!$response->successful()) {
            throw new \RuntimeException('Failed to fetch table list: ' . $response->status());
        }

        return $response->json('tables', []);
    }

    /**
     * Fetch paginated data for a specific table
     */
    public function fetchTableData(string $table, int $page = 1): array
    {
        $response = Http::timeout(120)
            ->withHeaders($this->getHeaders())
            ->post($this->getEndpoint("data/{$table}"), [
                'page' => $page,
            ]);

        if (!$response->successful()) {
            throw new \RuntimeException("Failed to fetch data for table {$table}: " . $response->status());
        }

        return $response->json();
    }

    /**
     * Fetch the list of the remote tenant's media files (path + size)
     *
     * @return array<int, array{path: string, size: int}>
     */
    public function getFileList(): array
    {
        $response = Http::timeout(120)
            ->withHeaders($this->getHeaders())
            ->post($this->getEndpoint('files'));

        if (!$response->successful()) {
            throw new \RuntimeException('Failed to fetch file list: ' . $response->status());
        }

        return $response->json('files', []);
    }

    /**
     * Download a single media file, preferring its direct storage URL so the
     * bytes travel object-storage -> local rather than being proxied through
     * the remote environment. Falls back to streaming through the remote when
     * no usable URL is available or the direct download fails (e.g. an expired
     * signed URL).
     *
     * @param array{path: string, size?: int, url?: string|null} $file
     */
    public function downloadFile(array $file): string
    {
        $url = $file['url'] ?? null;

        if (!empty($url)) {
            $response = Http::timeout(300)->get($url);

            if ($response->successful()) {
                return $response->body();
            }
        }

        return $this->fetchFile($file['path']);
    }

    /**
     * Download the contents of a single remote media file by streaming it
     * through the remote environment (fallback for downloadFile()).
     */
    public function fetchFile(string $path): string
    {
        $response = Http::timeout(300)
            ->withHeaders($this->getHeaders())
            ->post($this->getEndpoint('file'), [
                'path' => $path,
            ]);

        if (!$response->successful()) {
            throw new \RuntimeException("Failed to download file {$path}: " . $response->status());
        }

        return $response->body();
    }

    /**
     * Store a downloaded media file locally at its original path. Paths keep
     * the remote tenant prefix so they line up with the imported media rows.
     */
    public function importFile(string $path, string $contents): void
    {
        Storage::disk($this->repository->mediaDisk())->put($path, $contents);
    }

    /**
     * Import a table's data locally with truncation
     */
    public function importTable(string $table, array $records): int
    {
        if (empty($records)) {
            return 0;
        }

        // Special handling for settings table
        if ($table === 'settings') {
            return $this->importSettings($records);
        }

        $records = $this->encodeRedactedJsonColumns($table, $records);

        // Standard table import
        DB::table($table)->insert($records);

        return count($records);
    }

    /**
     * The redacted views replace populated JSON columns with a bare 'REDACTED'
     * marker, which local json_valid CHECK constraints reject. JSON-encode the
     * marker for those columns so the rows still insert.
     */
    private function encodeRedactedJsonColumns(string $table, array $records): array
    {
        $jsonColumns = $this->jsonCheckedColumns($table);

        if (empty($jsonColumns)) {
            return $records;
        }

        return array_map(function (array $record) use ($jsonColumns) {
            foreach ($jsonColumns as $column) {
                if (($record[$column] ?? null) === 'REDACTED') {
                    $record[$column] = json_encode('REDACTED');
                }
            }

            return $record;
        }, $records);
    }

    /**
     * Columns on the local table guarded by a json_valid CHECK constraint.
     * MariaDB names column-level check constraints after the column.
     */
    private function jsonCheckedColumns(string $table): array
    {
        try {
            return DB::table('information_schema.CHECK_CONSTRAINTS')
                ->where('CONSTRAINT_SCHEMA', DB::getDatabaseName())
                ->where('TABLE_NAME', $table)
                ->where('CHECK_CLAUSE', 'like', '%json_valid%')
                ->pluck('CONSTRAINT_NAME')
                ->all();
        } catch (\Exception) {
            return [];
        }
    }

    /**
     * Truncate a table before import
     */
    public function truncateTable(string $table): void
    {
        if (!Schema::hasTable($table)) {
            return;
        }

        DB::statement('SET FOREIGN_KEY_CHECKS=0');
        DB::table($table)->truncate();
        DB::statement('SET FOREIGN_KEY_CHECKS=1');
    }

    /**
     * Import settings with special handling for encrypted secrets
     */
    private function importSettings(array $records): int
    {
        $imported = 0;

        foreach ($records as $record) {
            $isEncryptedWithToken = $record['encrypted_with_token'] ?? false;
            unset($record['encrypted_with_token']);

            // Handle secret decryption
            if ($isEncryptedWithToken && ($record['type'] ?? '') === 'secret') {
                $record['value'] = $this->repository->decryptWithToken($record['value'], $this->token);
            }

            // Use Setting model to properly encrypt secrets with local APP_KEY
            $setting = Setting::query()->updateOrCreate(
                ['config_key' => $record['config_key']],
                collect($record)->except(['id', 'created_at', 'updated_at'])->toArray()
            );

            if ($setting) {
                $imported++;
            }
        }

        return $imported;
    }

    /**
     * Get HTTP headers for API requests
     */
    private function getHeaders(): array
    {
        return [
            'x-token' => $this->token,
            'x-user' => $this->email,
            'Accept' => 'application/json',
            'Content-Type' => 'application/json',
        ];
    }

    /**
     * Build full endpoint URL
     */
    private function getEndpoint(string $path): string
    {
        return "{$this->baseUrl}/api/site-export/{$this->tenantId}/{$path}";
    }
}
