<?php

namespace Licenzo\Services;

use Licenzo\Framework\Support\Facades\DB;
use Licenzo\Model\Customer;
use Licenzo\Model\IdempotencyKey;
use Licenzo\Model\Invoice;
use Licenzo\Model\InvoiceItem;
use Licenzo\Model\License;
use Licenzo\Model\LicenseActivation;
use Licenzo\Model\LicenseEvent;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Meta;
use Licenzo\Model\Product;
use Licenzo\Model\ProductVariation;
use Licenzo\Model\Subscription;
use Licenzo\Model\SubscriptionEvent;
use Licenzo\Model\SubscriptionProduct;
use Licenzo\Model\SubscriptionSchedule;
use Licenzo\Model\SubscriptionTransaction;
use Licenzo\Model\WebhookLog;

if (!defined('ABSPATH')) {
    exit;
}

class DataPortService
{
    const FORMAT_VERSION = '1.0';
    const CHUNK_SIZE = 500;
    const BATCH_SIZE = 200;

    const CATEGORIES = [
        'products' => [
            'label'     => 'Products',
            'mandatory' => true,
            'tables'    => [
                ['model' => Product::class,          'key' => 'products',           'fk' => []],
                ['model' => ProductVariation::class,  'key' => 'product_variations', 'fk' => ['product_id' => 'products']],
            ],
        ],
        'customers' => [
            'label'     => 'Customers',
            'mandatory' => true,
            'tables'    => [
                ['model' => Customer::class, 'key' => 'customers', 'fk' => []],
            ],
        ],
        'subscriptions' => [
            'label'     => 'Subscriptions',
            'mandatory' => true,
            'tables'    => [
                ['model' => Subscription::class,        'key' => 'subscriptions',        'fk' => ['customer_id' => 'customers']],
                ['model' => SubscriptionProduct::class,  'key' => 'subscription_products', 'fk' => ['subscription_id' => 'subscriptions', 'product_id' => 'products', 'variation_id' => 'product_variations']],
            ],
        ],
        'licenses' => [
            'label'     => 'Licenses',
            'mandatory' => true,
            'tables'    => [
                ['model' => License::class,            'key' => 'licenses',            'fk' => ['customer_id' => 'customers', 'subscription_id' => 'subscriptions']],
                ['model' => LicenseProduct::class,      'key' => 'license_products',    'fk' => ['license_id' => 'licenses', 'product_id' => 'products', 'variation_id' => 'product_variations']],
                ['model' => LicenseActivation::class,   'key' => 'license_activations', 'fk' => ['license_id' => 'licenses', 'product_id' => 'products', 'variation_id' => 'product_variations']],
            ],
        ],
        'transactions' => [
            'label'     => 'Transactions',
            'mandatory' => false,
            'tables'    => [
                ['model' => SubscriptionTransaction::class, 'key' => 'subscription_transactions', 'fk' => ['subscription_id' => 'subscriptions', 'customer_id' => 'customers', 'invoice_id' => 'invoices']],
            ],
        ],
        'invoices' => [
            'label'     => 'Invoices',
            'mandatory' => false,
            'tables'    => [
                ['model' => Invoice::class,     'key' => 'invoices',     'fk' => ['subscription_id' => 'subscriptions', 'customer_id' => 'customers']],
                ['model' => InvoiceItem::class,  'key' => 'invoice_items', 'fk' => ['invoice_id' => 'invoices']],
            ],
        ],
        'settings' => [
            'label'     => 'Settings',
            'mandatory' => false,
            'tables'    => [],
        ],
    ];

    /**
     * Flat lookup: table_key => ['model' => ..., 'fk' => ...]
     */
    private function getTableConfig(string $tableKey): ?array
    {
        foreach (static::CATEGORIES as $cat) {
            foreach ($cat['tables'] as $tbl) {
                if ($tbl['key'] === $tableKey) {
                    return $tbl;
                }
            }
        }
        return null;
    }

    // ── Directory helpers ────────────────────────────────────────

    private function getTmpDir(): string
    {
        $dir = wp_upload_dir()['basedir'] . '/licenzo-tmp';
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
            file_put_contents($dir . '/.htaccess', 'deny from all');
            file_put_contents($dir . '/index.php', '<?php // Silence is golden');
        }
        return $dir;
    }

    private function getBackupDir(): string
    {
        $dir = wp_upload_dir()['basedir'] . '/licenzo-backups';
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
            file_put_contents($dir . '/.htaccess', 'deny from all');
            file_put_contents($dir . '/index.php', '<?php // Silence is golden');
        }
        return $dir;
    }

    private function cleanOldTempFiles(): void
    {
        $dir = $this->getTmpDir();
        $cutoff = time() - 3600;

        foreach (glob($dir . '/export-*.json') as $file) {
            if (filemtime($file) < $cutoff) {
                wp_delete_file($file);
            }
        }

        foreach (glob($dir . '/import-*', GLOB_ONLYDIR) as $importDir) {
            if (filemtime($importDir) < $cutoff) {
                $this->removeDir($importDir);
            }
        }
    }

    private function removeDir(string $dir): void
    {
        if (!is_dir($dir)) return;
        foreach (scandir($dir) as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . '/' . $item;
            is_dir($path) ? $this->removeDir($path) : wp_delete_file($path);
        }
        @rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
    }

    // ── Categories ───────────────────────────────────────────────

    public function categories(): array
    {
        $result = [];

        foreach (static::CATEGORIES as $key => $cat) {
            $count = 0;
            if ($key === 'settings') {
                $count = Meta::where('entity_type', 'settings')->count();
            } else {
                $primary = $cat['tables'][0] ?? null;
                if ($primary) {
                    $count = $primary['model']::count();
                }
            }

            $result[] = [
                'key'       => $key,
                'label'     => $cat['label'],
                'mandatory' => $cat['mandatory'],
                'count'     => $count,
            ];
        }

        return $result;
    }

    // ── Export ────────────────────────────────────────────────────

    public function export(array $selectedKeys): array
    {
        $this->cleanOldTempFiles();

        $allMandatory = array_keys(array_filter(static::CATEGORIES, fn ($c) => $c['mandatory']));
        $selected = array_unique(array_merge($allMandatory, $selectedKeys));

        $data = [];
        $summary = [];

        foreach ($selected as $catKey) {
            $cat = static::CATEGORIES[$catKey] ?? null;
            if (!$cat) continue;

            if ($catKey === 'settings') {
                $rows = Meta::where('entity_type', 'settings')->get()
                    ->map(fn ($r) => $r->getRawOriginal())->toArray();
                $data['settings'] = $rows;
                $summary['settings'] = count($rows);
                continue;
            }

            foreach ($cat['tables'] as $tbl) {
                $tableRows = [];
                $offset = 0;

                while (true) {
                    $chunk = $tbl['model']::orderBy('id')
                        ->offset($offset)
                        ->limit(static::CHUNK_SIZE)
                        ->get();

                    if ($chunk->isEmpty()) break;

                    foreach ($chunk as $row) {
                        $tableRows[] = $row->getRawOriginal();
                    }

                    $offset += static::CHUNK_SIZE;

                    if ($chunk->count() < static::CHUNK_SIZE) break;
                }

                $data[$tbl['key']] = $tableRows;
                $summary[$tbl['key']] = count($tableRows);
            }
        }

        return [
            'format_version' => static::FORMAT_VERSION,
            'plugin_version' => defined('LICENZO_VERSION') ? LICENZO_VERSION : '1.1.0',
            'exported_at'    => gmdate('Y-m-d\TH:i:s\Z'),
            'site_url'       => get_site_url(),
            'categories'     => array_values($selected),
            'summary'        => $summary,
            'data'           => $data,
        ];
    }

    // ── Import: Chunked Upload ───────────────────────────────────

    public function handleChunkUpload(array $file, int $chunkIndex, int $totalChunks, ?string $token): array
    {
        if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
            throw new \InvalidArgumentException('No valid file uploaded.');
        }

        if ($chunkIndex === 0 && !$token) {
            $token = bin2hex(random_bytes(16));
        }

        if (empty($token)) {
            throw new \InvalidArgumentException('Missing upload token.');
        }

        $token = preg_replace('/[^a-f0-9]/', '', $token);
        $dir = $this->getTmpDir() . '/import-' . $token;

        if ($chunkIndex === 0) {
            wp_mkdir_p($dir);
        }

        if (!is_dir($dir)) {
            throw new \InvalidArgumentException('Upload session not found.');
        }

        $chunkPath = $dir . '/chunk-' . str_pad($chunkIndex, 5, '0', STR_PAD_LEFT);
        if (!copy($file['tmp_name'], $chunkPath)) {
            throw new \RuntimeException('Failed to save uploaded chunk.');
        }

        if ($chunkIndex < $totalChunks - 1) {
            return [
                'token'    => $token,
                'status'   => 'uploading',
                'received' => $chunkIndex + 1,
                'total'    => $totalChunks,
            ];
        }

        return $this->assembleAndPrepare($token, $dir, $totalChunks);
    }

    private function assembleAndPrepare(string $token, string $dir, int $totalChunks): array
    {
        $assembledPath = $dir . '/assembled.json';
        $fp = fopen($assembledPath, 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen

        for ($i = 0; $i < $totalChunks; $i++) {
            $chunkPath = $dir . '/chunk-' . str_pad($i, 5, '0', STR_PAD_LEFT);
            if (!file_exists($chunkPath)) {
                fclose($fp); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
                throw new \InvalidArgumentException('Missing chunk ' . (int) $i . '.');
            }
            fwrite($fp, file_get_contents($chunkPath)); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
            wp_delete_file($chunkPath);
        }

        fclose($fp); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose

        $content = file_get_contents($assembledPath);
        $payload = json_decode($content, true);
        unset($content);
        wp_delete_file($assembledPath);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \InvalidArgumentException('File is not valid JSON.');
        }

        $this->validateFormat($payload);

        $meta = [
            'format_version' => $payload['format_version'],
            'plugin_version' => $payload['plugin_version'] ?? null,
            'exported_at'    => $payload['exported_at'] ?? null,
            'site_url'       => $payload['site_url'] ?? null,
            'categories'     => $payload['categories'] ?? [],
            'summary'        => $payload['summary'] ?? [],
        ];

        file_put_contents($dir . '/meta.json', wp_json_encode($meta));

        $tableOrder = [];
        foreach ($payload['data'] as $key => $rows) {
            $safeKey = basename($key);
            file_put_contents($dir . '/' . $safeKey . '.json', wp_json_encode($rows));
            $tableOrder[] = ['key' => $safeKey, 'count' => count($rows)];
        }

        unset($payload);

        $state = [
            'status'              => 'pending',
            'tables'              => $tableOrder,
            'current_table_index' => 0,
            'current_row_offset'  => 0,
            'total'               => array_sum(array_column($tableOrder, 'count')),
            'processed'           => 0,
            'backup_done'         => false,
            'truncated'           => false,
            'id_map'              => [],
            'counts'              => [],
        ];

        file_put_contents($dir . '/state.json', wp_json_encode($state));

        return [
            'token'   => $token,
            'status'  => 'ready',
            'summary' => $meta,
        ];
    }

    // ── Import: Process batch ────────────────────────────────────

    public function processNextBatch(string $token): array
    {
        $token = preg_replace('/[^a-f0-9]/', '', $token);
        $dir = $this->getTmpDir() . '/import-' . $token;

        if (!is_dir($dir)) {
            throw new \InvalidArgumentException('Import session not found or expired.');
        }

        $state = json_decode(file_get_contents($dir . '/state.json'), true);

        if ($state['status'] === 'complete') {
            return $this->processResponse($state);
        }

        if (!$state['backup_done']) {
            $this->createAutoBackup();
            $state['backup_done'] = true;
            $this->saveState($dir, $state);
        }

        if (!$state['truncated']) {
            $this->truncateAll();
            $state['truncated'] = true;
            $this->saveState($dir, $state);
        }

        $state['status'] = 'processing';

        $tables    = $state['tables'];
        $idx       = $state['current_table_index'];
        $offset    = $state['current_row_offset'];
        $idMap     = $state['id_map'];
        $counts    = $state['counts'];
        $processed = $state['processed'];
        $remaining = static::BATCH_SIZE;

        while ($remaining > 0 && $idx < count($tables)) {
            $tableInfo = $tables[$idx];
            $tableKey  = $tableInfo['key'];

            $dataFile = $dir . '/' . basename($tableKey) . '.json';
            if (!file_exists($dataFile)) {
                $idx++;
                $offset = 0;
                continue;
            }

            $allRows = json_decode(file_get_contents($dataFile), true);

            if ($tableKey === 'settings') {
                $batch = array_slice($allRows, $offset);
                $settingsCount = $this->importRaw(Meta::class, $batch);
                $counts[$tableKey] = ($counts[$tableKey] ?? 0) + $settingsCount;
                $processed += $settingsCount;
                $idx++;
                $offset = 0;
                unset($allRows);
                continue;
            }

            $batch = array_slice($allRows, $offset, $remaining);
            unset($allRows);

            if (empty($batch)) {
                $idx++;
                $offset = 0;
                continue;
            }

            $tableConfig = $this->getTableConfig($tableKey);
            if (!$tableConfig) {
                $idx++;
                $offset = 0;
                continue;
            }

            $batch = $this->remapForeignKeys($batch, $tableConfig['fk'], $idMap);
            $result = $this->importWithIdTracking($tableConfig['model'], $batch);

            if (!isset($idMap[$tableKey])) {
                $idMap[$tableKey] = [];
            }
            $idMap[$tableKey] = $idMap[$tableKey] + $result['id_map'];

            $counts[$tableKey] = ($counts[$tableKey] ?? 0) + $result['count'];
            $processed += $result['count'];
            $offset += count($batch);
            $remaining -= count($batch);

            if ($offset >= $tableInfo['count']) {
                $idx++;
                $offset = 0;
            }
        }

        $state['current_table_index'] = $idx;
        $state['current_row_offset']  = $offset;
        $state['processed']           = $processed;
        $state['id_map']              = $idMap;
        $state['counts']              = $counts;

        if ($idx >= count($tables)) {
            $state['status'] = 'complete';
        }

        $this->saveState($dir, $state);

        if ($state['status'] === 'complete') {
            $this->cleanupImportData($dir);
        }

        return $this->processResponse($state);
    }

    // ── Auto-backup ──────────────────────────────────────────────

    private function createAutoBackup(): void
    {
        $backupDir = $this->getBackupDir();
        $allKeys = array_keys(static::CATEGORIES);

        $data = [];
        $summary = [];

        foreach ($allKeys as $catKey) {
            $cat = static::CATEGORIES[$catKey];

            if ($catKey === 'settings') {
                $rows = Meta::where('entity_type', 'settings')->get()
                    ->map(fn ($r) => $r->getRawOriginal())->toArray();
                $data['settings'] = $rows;
                $summary['settings'] = count($rows);
                continue;
            }

            foreach ($cat['tables'] as $tbl) {
                $rows = $tbl['model']::all()->map(fn ($r) => $r->getRawOriginal())->toArray();
                $data[$tbl['key']] = $rows;
                $summary[$tbl['key']] = count($rows);
            }
        }

        $envelope = [
            'format_version' => static::FORMAT_VERSION,
            'plugin_version' => defined('LICENZO_VERSION') ? LICENZO_VERSION : '1.1.0',
            'exported_at'    => gmdate('Y-m-d\TH:i:s\Z'),
            'site_url'       => get_site_url(),
            'categories'     => $allKeys,
            'summary'        => $summary,
            'data'           => $data,
        ];

        $filename = 'auto-backup-' . gmdate('Y-m-d-His') . '.json';
        file_put_contents(
            $backupDir . '/' . $filename,
            wp_json_encode($envelope, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
        );
    }

    // ── Internals ────────────────────────────────────────────────

    private function validateFormat(array $payload): void
    {
        if (empty($payload['format_version'])) {
            throw new \InvalidArgumentException('Invalid export file — missing format version.');
        }
        if (empty($payload['data']) || !is_array($payload['data'])) {
            throw new \InvalidArgumentException('Invalid export file — missing data.');
        }

        $allowed = $this->getAllowedDataKeys();
        $unknown = array_diff(array_keys($payload['data']), $allowed);

        if (!empty($unknown)) {
            $safeKeys = array_map('sanitize_text_field', $unknown);
            throw new \InvalidArgumentException(
                'Invalid export file — unrecognized data keys: ' . implode(', ', $safeKeys) . '.' // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Already sanitized above.
            );
        }
    }

    /**
     * Derive the set of valid `$payload['data']` keys from CATEGORIES.
     *
     * Built dynamically so the allowlist stays in sync if a future
     * release adds a new category or table — no second hardcoded list
     * to drift out of date.
     *
     * @return string[]
     */
    private function getAllowedDataKeys(): array
    {
        $keys = [];

        foreach (static::CATEGORIES as $catKey => $cat) {
            if (empty($cat['tables'])) {
                $keys[] = $catKey;
                continue;
            }
            foreach ($cat['tables'] as $tbl) {
                $keys[] = $tbl['key'];
            }
        }

        return $keys;
    }

    private function truncateAll(): void
    {
        $models = [
            InvoiceItem::class,
            Invoice::class,
            SubscriptionSchedule::class,
            SubscriptionEvent::class,
            SubscriptionTransaction::class,
            LicenseEvent::class,
            LicenseActivation::class,
            LicenseProduct::class,
            License::class,
            SubscriptionProduct::class,
            Subscription::class,
            Customer::class,
            ProductVariation::class,
            Product::class,
            IdempotencyKey::class,
            WebhookLog::class,
            Meta::class,
        ];

        foreach ($models as $model) {
            DB::table((new $model())->getTable())->truncate();
        }
    }

    private function remapForeignKeys(array $rows, array $fkMap, array $idMap): array
    {
        if (empty($fkMap)) return $rows;

        return array_map(function (array $row) use ($fkMap, $idMap) {
            foreach ($fkMap as $column => $sourceKey) {
                if (empty($row[$column])) continue;
                $row[$column] = $idMap[$sourceKey][(int) $row[$column]] ?? null;
            }
            return $row;
        }, $rows);
    }

    private function importWithIdTracking(string $model, array $rows): array
    {
        $map = [];
        $count = 0;

        foreach ($rows as $row) {
            $oldId = $row['id'] ?? null;
            unset($row['id']);

            $instance = new $model();
            $instance->setRawAttributes($row);
            $instance->save();

            if ($oldId !== null) {
                $map[(int) $oldId] = $instance->id;
            }
            $count++;
        }

        return ['id_map' => $map, 'count' => $count];
    }

    private function importRaw(string $model, array $rows): int
    {
        $count = 0;
        foreach ($rows as $row) {
            unset($row['id']);
            $instance = new $model();
            $instance->setRawAttributes($row);
            $instance->save();
            $count++;
        }
        return $count;
    }

    private function cleanupImportData(string $dir): void
    {
        if (!is_dir($dir)) return;

        foreach (glob($dir . '/*.json') as $file) {
            if (basename($file) !== 'state.json') {
                wp_delete_file($file);
            }
        }
    }

    private function saveState(string $dir, array $state): void
    {
        if (is_dir($dir)) {
            file_put_contents($dir . '/state.json', wp_json_encode($state));
        }
    }

    private function processResponse(array $state): array
    {
        return [
            'status'    => $state['status'],
            'processed' => $state['processed'],
            'total'     => $state['total'],
            'counts'    => $state['counts'],
        ];
    }
}
