<?php

namespace App\Services\ImportExport;

use App\Contracts\ImportExport\ModuleHandler;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ImportService
{
    public function __construct(
        private readonly ModuleRegistry $registry,
        private readonly ImportExportLogService $logger,
        private readonly TemplateService $templates,
    ) {}

    public function downloadTemplate(User $user, string $module): StreamedResponse
    {
        $handler = $this->registry->get($module);
        $this->authorize($user, $handler, 'create');

        if (! $handler->supportsImport()) {
            abort(404, 'This module does not support import.');
        }

        return $this->templates->download($handler);
    }

    /**
     * @return array{
     *     token: string,
     *     module: string,
     *     file_name: string,
     *     total_rows: int,
     *     valid_rows: int,
     *     invalid_rows: int,
     *     preview: list<array{row_number: int, data: array<string, mixed>, errors: list<array<string, mixed>>, action: string}>,
     *     errors: list<array<string, mixed>>
     * }
     */
    public function preview(User $user, string $module, UploadedFile $file): array
    {
        $handler = $this->registry->get($module);
        $this->authorize($user, $handler, 'create');

        if (! $handler->supportsImport()) {
            throw new InvalidArgumentException('This module does not support import.');
        }

        $rows = $this->parseFile($file, $handler);
        $preview = [];
        $valid = 0;
        $invalid = 0;
        $allErrors = [];

        foreach ($rows as $index => $row) {
            $rowNumber = $index + 2;
            $errors = $handler->validateImportRow($row, $rowNumber);

            if ($errors === []) {
                $valid++;
                $action = 'create';
            } else {
                $invalid++;
                $action = 'skip';
                foreach ($errors as $error) {
                    $allErrors[] = array_merge($error, ['row_number' => $rowNumber]);
                }
            }

            if (count($preview) < 100) {
                $preview[] = [
                    'row_number' => $rowNumber,
                    'data' => $row,
                    'errors' => $errors,
                    'action' => $action,
                ];
            }
        }

        $token = (string) str()->uuid();

        session()->put($this->sessionKey($token), [
            'module' => $module,
            'file_name' => $file->getClientOriginalName(),
            'rows' => $rows,
            'user_id' => $user->id,
        ]);

        return [
            'token' => $token,
            'module' => $module,
            'file_name' => $file->getClientOriginalName(),
            'total_rows' => count($rows),
            'valid_rows' => $valid,
            'invalid_rows' => $invalid,
            'preview' => $preview,
            'errors' => $allErrors,
        ];
    }

    /**
     * @return array{created: int, updated: int, skipped: int, errors: list<string>}
     */
    public function confirm(User $user, string $token): array
    {
        $payload = session()->pull($this->sessionKey($token));

        if ($payload === null || ($payload['user_id'] ?? null) !== $user->id) {
            throw new InvalidArgumentException('Import session expired. Please upload the file again.');
        }

        $handler = $this->registry->get($payload['module']);
        $this->authorize($user, $handler, 'create');

        $created = 0;
        $updated = 0;
        $skipped = 0;
        $errors = [];

        if ($payload['module'] === 'journal-entries') {
            /** @var \App\Services\ImportExport\Handlers\JournalEntriesHandler $handler */
            $result = $handler->importGroupedRows($payload['rows'], $user);

            $this->logger->logImport(
                $user,
                $payload['module'],
                $payload['file_name'],
                count($payload['rows']),
                $result['created'] + $result['updated'],
                $result['skipped'],
                $result['errors'] === [] ? null : collect($result['errors'])->take(20)->implode("\n"),
                request(),
            );

            return $result;
        }

        DB::transaction(function () use ($handler, $payload, &$created, &$updated, &$skipped, &$errors) {
            foreach ($payload['rows'] as $index => $row) {
                $rowNumber = $index + 2;
                $validationErrors = $handler->validateImportRow($row, $rowNumber);

                if ($validationErrors !== []) {
                    $skipped++;
                    $errors[] = "Row {$rowNumber}: ".collect($validationErrors)->pluck('message')->implode('; ');

                    continue;
                }

                try {
                    $result = $handler->importRow($row);

                    if ($result['action'] === 'updated') {
                        $updated++;
                    } else {
                        $created++;
                    }
                } catch (\Throwable $e) {
                    $skipped++;
                    $errors[] = "Row {$rowNumber}: {$e->getMessage()}";
                }
            }
        });

        $total = count($payload['rows']);
        $success = $created + $updated;

        $this->logger->logImport(
            $user,
            $payload['module'],
            $payload['file_name'],
            $total,
            $success,
            $skipped,
            $errors === [] ? null : collect($errors)->take(20)->implode("\n"),
            request(),
        );

        return compact('created', 'updated', 'skipped', 'errors');
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function parseFile(UploadedFile $file, ModuleHandler $handler): array
    {
        $extension = strtolower($file->getClientOriginalExtension());
        $allowed = ['xlsx', 'xls', 'csv', 'txt'];

        if (! in_array($extension, $allowed, true)) {
            throw new InvalidArgumentException('Please upload an Excel (.xlsx) or CSV file.');
        }

        $sheets = Excel::toArray(new \stdClass, $file);
        $sheet = $sheets[0] ?? [];

        if ($sheet === []) {
            return [];
        }

        $headerRow = null;
        $headerIndex = 0;

        foreach ($sheet as $index => $row) {
            if ($this->isHeaderRow($row, $handler)) {
                $headerRow = array_map(fn ($h) => $this->normalizeHeader((string) $h), $row);
                $headerIndex = $index;

                break;
            }
        }

        if ($headerRow === null) {
            throw new InvalidArgumentException('Could not find column headers in the uploaded file.');
        }

        $rows = [];

        foreach (array_slice($sheet, $headerIndex + 1) as $row) {
            if ($this->isEmptyRow($row)) {
                continue;
            }

            $assoc = [];
            foreach ($headerRow as $colIndex => $key) {
                if ($key === '') {
                    continue;
                }
                $assoc[$key] = isset($row[$colIndex]) ? trim((string) $row[$colIndex]) : null;
            }

            $rows[] = $assoc;
        }

        return $rows;
    }

    /**
     * @param  list<mixed>  $row
     */
    private function isHeaderRow(array $row, ModuleHandler $handler): bool
    {
        $expected = collect($handler->importColumns())->pluck('key')->all();
        $normalized = array_map(fn ($h) => $this->normalizeHeader((string) $h), $row);
        $matches = count(array_intersect($expected, $normalized));

        return $matches >= max(2, (int) ceil(count($expected) * 0.5));
    }

    /**
     * @param  list<mixed>  $row
     */
    private function isEmptyRow(array $row): bool
    {
        return trim(implode('', array_map(fn ($v) => (string) $v, $row))) === '';
    }

    private function normalizeHeader(string $header): string
    {
        return str($header)->lower()->trim()->replace(' ', '_')->toString();
    }

    private function sessionKey(string $token): string
    {
        return "import_export.preview.{$token}";
    }

    private function authorize(User $user, ModuleHandler $handler, string $action): void
    {
        if ($user->hasRole('Super Admin')) {
            return;
        }

        if (! $user->can("{$handler->permissionModule()}.{$action}")) {
            abort(403);
        }
    }
}
