<?php

declare(strict_types=1);

namespace WhmcsMigrate;

use RuntimeException;

/**
 * Manages JSONL export files for offline/fast re-imports.
 *
 * Each entity type (clients, products, invoices, etc.) gets its own JSONL file.
 * Each line is a self-contained JSON object representing one API response batch.
 */
final class ExportManager
{
    private string $exportDir;

    /** @var array<string, resource> Open file handles for writing */
    private array $writeHandles = [];

    /** @var array<string, resource> Open file handles for reading */
    private array $readHandles = [];

    /** @var array<string, int> Number of records written per entity */
    private array $writeCounts = [];

    public function __construct(string $basePath)
    {
        $this->exportDir = $basePath . '/exports';

        if (! is_dir($this->exportDir)) {
            mkdir($this->exportDir, 0755, true);
        }
    }

    /**
     * Get the export directory path.
     */
    public function getExportDir(): string
    {
        return $this->exportDir;
    }

    /**
     * Write a single record to a JSONL export file.
     *
     * @param string $entity Entity type (e.g., 'clients', 'products', 'invoices')
     * @param array<string, mixed> $record The record data to write
     */
    public function writeRecord(string $entity, array $record): void
    {
        $handle = $this->getWriteHandle($entity);
        $line = json_encode($record, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) . "\n";

        $written = fwrite($handle, $line);

        if ($written === false) {
            throw new RuntimeException("Failed to write to export file for entity '{$entity}'");
        }

        $this->writeCounts[$entity] = ($this->writeCounts[$entity] ?? 0) + 1;
    }

    /**
     * Write a batch of records to a JSONL export file.
     *
     * @param string $entity Entity type
     * @param array<int, array<string, mixed>> $records The records to write
     */
    public function writeBatch(string $entity, array $records): void
    {
        foreach ($records as $record) {
            $this->writeRecord($entity, $record);
        }
    }

    /**
     * Read all records from a JSONL export file as a generator.
     *
     * This uses a generator to avoid loading the entire file into memory.
     *
     * @param string $entity Entity type
     * @return \Generator<int, array<string, mixed>>
     */
    public function readRecords(string $entity): \Generator
    {
        $path = $this->getFilePath($entity);

        if (! file_exists($path)) {
            throw new RuntimeException("Export file not found: {$path}. Run --export-only first.");
        }

        $handle = fopen($path, 'r');

        if ($handle === false) {
            throw new RuntimeException("Cannot open export file: {$path}");
        }

        $lineNumber = 0;

        try {
            while (($line = fgets($handle)) !== false) {
                $lineNumber++;
                $line = trim($line);

                if ($line === '') {
                    continue;
                }

                $decoded = json_decode($line, true);

                if (json_last_error() !== JSON_ERROR_NONE) {
                    throw new RuntimeException(
                        "Invalid JSON on line {$lineNumber} of {$path}: " . json_last_error_msg()
                    );
                }

                yield $decoded;
            }
        } finally {
            fclose($handle);
        }
    }

    /**
     * Read all records from a JSONL export file into an array.
     *
     * Use this only for small datasets (e.g., products, promotions).
     * For large datasets, use readRecords() generator instead.
     *
     * @param string $entity Entity type
     * @return array<int, array<string, mixed>>
     */
    public function readAll(string $entity): array
    {
        $records = [];

        foreach ($this->readRecords($entity) as $record) {
            $records[] = $record;
        }

        return $records;
    }

    /**
     * Count the number of records in an export file without loading them all.
     */
    public function countRecords(string $entity): int
    {
        $path = $this->getFilePath($entity);

        if (! file_exists($path)) {
            return 0;
        }

        $count = 0;
        $handle = fopen($path, 'r');

        if ($handle === false) {
            return 0;
        }

        while (($line = fgets($handle)) !== false) {
            $line = trim($line);

            if ($line !== '') {
                $count++;
            }
        }

        fclose($handle);

        return $count;
    }

    /**
     * Check if an export file exists for a given entity.
     */
    public function hasExport(string $entity): bool
    {
        $path = $this->getFilePath($entity);

        return file_exists($path) && filesize($path) > 0;
    }

    /**
     * Get the number of records written so far for an entity.
     */
    public function getWriteCount(string $entity): int
    {
        return $this->writeCounts[$entity] ?? 0;
    }

    /**
     * Get a summary of all export files.
     *
     * @return array<string, array{file: string, records: int, size: string}>
     */
    public function getSummary(): array
    {
        $entities = ['clients', 'client_details', 'products', 'client_services', 'invoices', 'invoice_details', 'transactions', 'promotions', 'orders'];
        $summary = [];

        foreach ($entities as $entity) {
            $path = $this->getFilePath($entity);

            if (file_exists($path)) {
                $size = filesize($path);
                $records = $this->countRecords($entity);
                $summary[$entity] = [
                    'file' => basename($path),
                    'records' => $records,
                    'size' => $this->formatBytes($size),
                ];
            }
        }

        return $summary;
    }

    /**
     * Reset (delete) the export file for a given entity.
     */
    public function resetExport(string $entity): void
    {
        $this->closeWriteHandle($entity);

        $path = $this->getFilePath($entity);

        if (file_exists($path)) {
            unlink($path);
        }

        unset($this->writeCounts[$entity]);
    }

    /**
     * Reset all export files.
     */
    public function resetAll(): void
    {
        $this->closeAllHandles();

        $files = glob($this->exportDir . '/*.jsonl');

        if (is_array($files)) {
            foreach ($files as $file) {
                unlink($file);
            }
        }

        $this->writeCounts = [];
    }

    /**
     * Flush and close all open file handles.
     */
    public function closeAllHandles(): void
    {
        foreach (array_keys($this->writeHandles) as $entity) {
            $this->closeWriteHandle($entity);
        }
    }

    /**
     * Get the JSONL file path for a given entity type.
     */
    public function getFilePath(string $entity): string
    {
        return $this->exportDir . '/' . $entity . '.jsonl';
    }

    /**
     * Get or create a write handle for the given entity.
     *
     * @return resource
     */
    private function getWriteHandle(string $entity)
    {
        if (! isset($this->writeHandles[$entity])) {
            $path = $this->getFilePath($entity);
            $handle = fopen($path, 'a');

            if ($handle === false) {
                throw new RuntimeException("Cannot open export file for writing: {$path}");
            }

            $this->writeHandles[$entity] = $handle;
        }

        return $this->writeHandles[$entity];
    }

    /**
     * Close a write handle for the given entity.
     */
    private function closeWriteHandle(string $entity): void
    {
        if (isset($this->writeHandles[$entity])) {
            fflush($this->writeHandles[$entity]);
            fclose($this->writeHandles[$entity]);
            unset($this->writeHandles[$entity]);
        }
    }

    /**
     * Format bytes into a human-readable string.
     */
    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
        $power = min($power, count($units) - 1);

        return round($bytes / pow(1024, $power), 2) . ' ' . $units[(int) $power];
    }
}
