<?php

declare(strict_types=1);

namespace App\Business\DataImport;

use App\Business\DataImport\Enums\DataImportBatchStatus;
use App\Business\DataImport\Enums\DataImportType;
use App\Business\DataImport\Support\DataImportErrorSanitizer;
use App\Business\DataImport\Support\DataImportValueParser;
use App\Models\DberpDataImportBatch;
use App\Models\DberpDataImportBatchRow;
use App\Support\DberpAuditCorrelationContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use RuntimeException;

final class DataImportOrchestrator
{
    public function __construct(
        private readonly DataImportFileReader $fileReader,
        private readonly DataImportHandlerRegistry $handlerRegistry,
        private readonly DataImportBatchNoGenerator $batchNoGenerator,
        private readonly DataImportAuditLogger $auditLogger,
    ) {}

    public function precheck(
        DataImportType $importType,
        string $absolutePath,
        string $sourceFilename,
        int $operatorUserId,
    ): DberpDataImportBatch {
        if (! $this->handlerRegistry->supports($importType)) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.execution_not_supported'),
            ]);
        }

        $readResult = $this->fileReader->read(
            importType: $importType,
            absolutePath: $absolutePath,
            sourceFilename: $sourceFilename,
        );

        $handler = $this->handlerRegistry->resolve($importType);
        $precheckResult = $handler->precheckRows($readResult->rows);
        $batchNo = $this->batchNoGenerator->generate();
        $storedPath = $this->storeSourceFile($batchNo, $absolutePath, $sourceFilename);
        $correlationId = (string) Str::uuid();

        $batch = DB::transaction(function () use (
            $batchNo,
            $importType,
            $sourceFilename,
            $storedPath,
            $operatorUserId,
            $correlationId,
            $readResult,
            $precheckResult,
        ): DberpDataImportBatch {
            $batch = DberpDataImportBatch::query()->create([
                'batch_no' => $batchNo,
                'import_type' => $importType,
                'status' => $precheckResult->passed()
                    ? DataImportBatchStatus::Prechecked
                    : DataImportBatchStatus::PrecheckFailed,
                'source_filename' => $sourceFilename,
                'stored_path' => $storedPath,
                'operator_user_id' => $operatorUserId,
                'total_rows' => count($readResult->rows),
                'success_rows' => $precheckResult->validatedCount,
                'failed_rows' => $precheckResult->failedCount,
                'error_summary' => $precheckResult->failedCount > 0
                    ? ['messages' => $precheckResult->summaryErrors()]
                    : null,
                'correlation_id' => $correlationId,
            ]);

            foreach ($precheckResult->rowOutcomes as $outcome) {
                $externalKey = $this->resolveBatchRowExternalKey($importType, $outcome);

                DberpDataImportBatchRow::query()->create([
                    'batch_id' => $batch->getKey(),
                    'row_number' => $outcome->rowNumber,
                    'status' => $outcome->status,
                    'raw_snapshot' => $outcome->rawSnapshot,
                    'error_messages' => $outcome->errorMessages !== [] ? $outcome->errorMessages : null,
                    'external_key' => $externalKey,
                ]);
            }

            return $batch->fresh(['rows']);
        });

        $this->auditLogger->logPrechecked($batch);

        return $batch;
    }

    public function execute(DberpDataImportBatch $batch, int $operatorUserId): DberpDataImportBatch
    {
        if ($batch->status !== DataImportBatchStatus::Prechecked) {
            throw ValidationException::withMessages([
                'batch' => (string) __('dberp.data_import.errors.batch_not_ready_for_execution'),
            ]);
        }

        if (! $this->handlerRegistry->supports($batch->import_type)) {
            throw ValidationException::withMessages([
                'batch' => (string) __('dberp.data_import.errors.unsupported_import_type'),
            ]);
        }

        $handler = $this->handlerRegistry->resolve($batch->import_type);
        $previousCorrelationId = DberpAuditCorrelationContext::id();

        DberpAuditCorrelationContext::reset($batch->correlation_id);

        $batch->update([
            'status' => DataImportBatchStatus::Processing,
            'operator_user_id' => $operatorUserId,
            'started_at' => now(),
            'finished_at' => null,
        ]);

        $this->auditLogger->logStarted($batch->fresh());

        try {
            $executionResult = $handler->execute($batch->fresh(['rows']));
        } catch (\Throwable $exception) {
            $reason = DataImportErrorSanitizer::executionFailureMessage($exception);

            $batch->update([
                'status' => DataImportBatchStatus::Failed,
                'finished_at' => now(),
                'error_summary' => [
                    'messages' => [(string) __('dberp.data_import.errors.execution_failed', [
                        'reason' => $reason,
                    ])],
                ],
            ]);

            $this->auditLogger->logFailed($batch->fresh(), $reason);

            DberpAuditCorrelationContext::reset($previousCorrelationId);

            throw $exception;
        }

        $batch->update([
            'status' => $executionResult->succeeded()
                ? DataImportBatchStatus::Completed
                : DataImportBatchStatus::Failed,
            'success_rows' => $executionResult->importedCount,
            'failed_rows' => $executionResult->failedCount,
            'finished_at' => now(),
            'error_summary' => $executionResult->failedCount > 0
                ? ['messages' => $this->collectExecutionErrors($executionResult)]
                : null,
        ]);

        $batch = $batch->fresh(['rows']);

        if ($batch->status === DataImportBatchStatus::Completed) {
            $this->auditLogger->logCompleted($batch);
        } else {
            $this->auditLogger->logFailed($batch);
        }

        DberpAuditCorrelationContext::reset($previousCorrelationId);

        return $batch;
    }

    private function storeSourceFile(string $batchNo, string $absolutePath, string $sourceFilename): string
    {
        $baseDirectory = trim((string) config('dberp.data_import.batch_upload_directory', 'dberp/imports'), '/');
        $extension = strtolower(pathinfo($sourceFilename, PATHINFO_EXTENSION));
        $safeExtension = in_array($extension, ['xlsx', 'csv'], true) ? $extension : 'dat';
        $storedPath = sprintf('%s/%s/source.%s', $baseDirectory, $batchNo, $safeExtension);

        $contents = file_get_contents($absolutePath);

        if ($contents === false) {
            throw new RuntimeException((string) __('dberp.data_import.errors.file_not_readable'));
        }

        Storage::disk('local')->put($storedPath, $contents);

        return $storedPath;
    }

    /**
     * @return list<string>
     */
    private function collectExecutionErrors(DataImportExecutionResult $executionResult): array
    {
        $errors = [];

        foreach ($executionResult->rowOutcomes as $outcome) {
            if ($outcome->imported()) {
                continue;
            }

            foreach ($outcome->errorMessages as $message) {
                $errors[] = $message;

                if (count($errors) >= 20) {
                    return $errors;
                }
            }
        }

        return $errors;
    }

    /**
     * @param  array<string, string|null>  $rawSnapshot
     */
    private function resolveBatchRowExternalKey(DataImportType $importType, DataImportRowPrecheckOutcome $outcome): string
    {
        if ($importType === DataImportType::OpeningStock) {
            return DataImportValueParser::trimmed($outcome->rawSnapshot['external_key'] ?? null)
                ?? 'row:'.$outcome->rowNumber;
        }

        return 'row:'.$outcome->rowNumber;
    }
}
