<?php

namespace MintyPHP\Service\Import;

use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\ImportAuditInterface;
use MintyPHP\Service\I18n\TranslatesServiceText;
use MintyPHP\Service\Import\Profile\ImportProfileInterface;
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
use MintyPHP\Service\Tenant\TenantScopeService;

/**
 * Orchestrates CSV imports through a three-phase workflow:
 *
 *   1. analyzeUpload() — validates file, detects delimiter/headers, stores temp file,
 *      returns a session-scoped token for subsequent calls.
 *   2. preview()       — maps columns → target fields, validates each row via the
 *      profile's dryRunRow(), returns would-create/skip/fail counts.
 *   3. commit()        — identical mapping + validation, then commitRow() for real
 *      writes; wrapped in an audit run (start → finish).
 *
 * Import behavior is pluggable via ImportProfileInterface. Each profile defines
 * allowed/required target fields, validation rules, and the actual create logic.
 * The PROFILE_PERMISSION_MAP links profile keys to the required permission.
 *
 * Cross-user token reuse is prevented by verifying user_id in preview/commit.
 * Assignment columns (tenant/role/department) require a separate permission check.
 */
class ImportService
{
    use TranslatesServiceText;

    private const MAX_ROWS = 20000;
    private const MAX_ERROR_ROWS = 500;
    private const PROFILE_PERMISSION_MAP = [
        'users' => PermissionService::USERS_IMPORT,
        'departments' => PermissionService::DEPARTMENTS_IMPORT,
    ];

    /**
     * @param array<string, ImportProfileInterface> $profiles
     */
    public function __construct(
        private readonly CsvReaderService $csvReaderService,
        private readonly ImportTempFileService $importTempFileService,
        private readonly ImportAuditInterface $importAuditService,
        private readonly ImportStateStoreService $importStateStoreService,
        private readonly array $profiles,
        private readonly PermissionService $permissionService,
        private readonly SettingsDefaultsGateway $settingsDefaultsGateway,
        private readonly TenantScopeService $userScopeGateway,
        private readonly SessionStoreInterface $sessionStore
    ) {
    }

    /**
     * @return array<string, ImportProfileInterface>
     */
    public function profiles(): array
    {
        return $this->profiles;
    }

    public function profile(string $type): ?ImportProfileInterface
    {
        $type = trim($type);
        if ($type === '') {
            return null;
        }
        return $this->profiles[$type] ?? null;
    }

    /**
     * @return array<int, array{key:string,label:string}>
     */
    public function listProfileOptions(): array
    {
        $options = [];
        foreach ($this->profiles() as $profile) {
            $options[] = [
                'key' => $profile->key(),
                'label' => $profile->label(),
            ];
        }
        return $options;
    }

    public function requiredPermissionForType(string $type): string
    {
        $type = trim($type);
        if ($type === '') {
            return '';
        }
        return self::PROFILE_PERMISSION_MAP[$type] ?? '';
    }

    /**
     * @param array<string, mixed> $upload
     * @return array<string, mixed>
     */
    public function analyzeUpload(array $upload, string $type, int $currentUserId): array
    {
        $profile = $this->profile($type);
        if (!$profile) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }

        $stored = $this->importTempFileService->storeUploadedFile($upload);
        if (!$stored['ok']) {
            $code = isset($stored['code']) ? (string) $stored['code'] : 'upload_missing';
            return ['ok' => false, 'code' => $code];
        }

        $path = (string) ($stored['path'] ?? '');
        $analysis = $this->csvReaderService->analyze($path, self::MAX_ROWS);
        if (!$analysis['ok']) {
            $this->importTempFileService->delete($path);
            $code = isset($analysis['code']) ? (string) $analysis['code'] : 'invalid_csv_header';
            return ['ok' => false, 'code' => $code];
        }

        $headers = $analysis['headers'] ?? [];
        $delimiter = (string) ($analysis['delimiter'] ?? ',');
        $rowsTotal = (int) ($analysis['rows_total'] ?? 0);

        $token = $this->newToken();
        $this->importStateStoreService->setState($token, [
            'token' => $token,
            'type' => $profile->key(),
            'path' => $path,
            'source_filename' => trim((string) ($upload['name'] ?? '')),
            'delimiter' => $delimiter,
            'headers' => $headers,
            'rows_total' => $rowsTotal,
            'user_id' => $currentUserId,
            'created_at' => time(),
        ]);

        return [
            'ok' => true,
            'token' => $token,
            'type' => $profile->key(),
            'headers' => $headers,
            'rows_total' => $rowsTotal,
            'delimiter' => $delimiter,
            'allowed_targets' => $profile->allowedTargets(),
            'required_targets' => $profile->requiredTargets(),
            'auto_mapping' => $this->buildAutoMapping($headers, $profile),
        ];
    }

    /**
     * @param array<string, mixed> $mappingInput
     * @return array<string, mixed>
     */
    public function preview(string $token, array $mappingInput, int $currentUserId): array
    {
        $state = $this->state($token);
        if (!$state) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }
        // Prevent cross-user token reuse — the state token is session-scoped but user_id is verified explicitly.
        if ((int) ($state['user_id'] ?? 0) !== $currentUserId) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }

        $profile = $this->profile((string) ($state['type'] ?? ''));
        if (!$profile) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }

        $mappingResult = $this->normalizeMapping($mappingInput, (array) ($state['headers'] ?? []), $profile);
        if (!($mappingResult['ok'] ?? false)) {
            return ['ok' => false, 'code' => (string) ($mappingResult['code'] ?? 'mapping_required_missing')];
        }

        $mappedTargets = $mappingResult['mapped_targets'] ?? [];
        // Tenant/role/department column mapping requires a separate permission — a user who can import
        // users generally may not be allowed to also assign them to tenants.
        if (
            $profile->requiresAssignmentPermission()
            && $this->usesAssignmentTargets($mappedTargets)
            && !$this->canImportAssignments($currentUserId)
        ) {
            return ['ok' => false, 'code' => 'assignment_permission_required'];
        }

        return $this->process($state, $mappingResult['mapping'], $profile, $currentUserId, false);
    }

    /**
     * @param array<string, mixed> $mappingInput
     * @return array<string, mixed>
     */
    public function commit(string $token, array $mappingInput, int $currentUserId): array
    {
        $state = $this->state($token);
        if (!$state) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }
        if ((int) ($state['user_id'] ?? 0) !== $currentUserId) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }

        $profile = $this->profile((string) ($state['type'] ?? ''));
        if (!$profile) {
            return ['ok' => false, 'code' => 'invalid_file_type'];
        }

        $mappingResult = $this->normalizeMapping($mappingInput, (array) ($state['headers'] ?? []), $profile);
        if (!($mappingResult['ok'] ?? false)) {
            return ['ok' => false, 'code' => (string) ($mappingResult['code'] ?? 'mapping_required_missing')];
        }

        $mappedTargets = $mappingResult['mapped_targets'] ?? [];
        if (
            $profile->requiresAssignmentPermission()
            && $this->usesAssignmentTargets($mappedTargets)
            && !$this->canImportAssignments($currentUserId)
        ) {
            return ['ok' => false, 'code' => 'assignment_permission_required'];
        }

        $auditRunId = $this->importAuditService->startRun(
            (string) ($state['type'] ?? $profile->key()),
            $mappingResult['mapped_targets'] ?? [],
            isset($state['source_filename']) ? (string) $state['source_filename'] : null,
            $currentUserId,
            $this->currentTenantId()
        );

        $result = ['ok' => false, 'code' => 'unexpected_error', 'processed' => 0, 'created' => 0, 'skipped' => 0, 'failed' => 1];
        $forcedAuditStatus = null;
        try {
            $result = $this->process($state, $mappingResult['mapping'], $profile, $currentUserId, true);
        } catch (\Throwable $exception) {
            $forcedAuditStatus = 'failed';
            $result = ['ok' => false, 'code' => 'unexpected_error', 'processed' => 0, 'created' => 0, 'skipped' => 0, 'failed' => 1];
        } finally {
            $this->importAuditService->finishRun($auditRunId, $result, $forcedAuditStatus);
        }

        $this->importTempFileService->delete((string) ($state['path'] ?? ''));
        $this->importStateStoreService->clearState($token);

        return $result;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function state(string $token): ?array
    {
        return $this->importStateStoreService->getState($token);
    }

    /**
     * @param array<string, mixed> $state
     * @param array<string, string> $mapping
     * @return array<string, mixed>
     */
    private function process(
        array $state,
        array $mapping,
        ImportProfileInterface $profile,
        int $currentUserId,
        bool $commit
    ): array {
        $path = (string) ($state['path'] ?? '');
        $delimiter = (string) ($state['delimiter'] ?? ',');
        $headers = $state['headers'] ?? [];
        if (!is_array($headers) || !$headers) {
            return ['ok' => false, 'code' => 'invalid_csv_header'];
        }

        $context = $this->buildContext($currentUserId);
        $seenRowKeys = [];
        $processed = 0;
        $created = 0;
        $skipped = 0;
        $failed = 0;
        $wouldCreate = 0;
        $errors = [];
        $errorCounts = [];

        $this->csvReaderService->streamRows(
            $path,
            $delimiter,
            array_values($headers),
            function (array $rowAssoc, int $lineNumber) use (
                $profile,
                $mapping,
                $context,
                $commit,
                &$seenRowKeys,
                &$processed,
                &$created,
                &$skipped,
                &$failed,
                &$wouldCreate,
                &$errors,
                &$errorCounts
            ): void {
                $processed++;
                $mapped = [];
                $identifier = '';
                try {
                    $mapped = $this->mapRow($rowAssoc, $mapping);
                    $validation = $profile->validateMappedRow($mapped, $context);
                    if (!$validation['ok']) {
                        $failed++;
                        foreach ($validation['errors'] as $error) {
                            $this->incrementErrorCount($errorCounts, (string) $error['code']);
                            $this->pushError(
                                $errors,
                                $lineNumber,
                                $profile->rowIdentifier($mapped),
                                (string) $error['code'],
                                isset($error['message']) ? (string) $error['message'] : null
                            );
                        }
                        return;
                    }

                    $normalized = $validation['normalized'];
                    $identifier = $profile->rowIdentifier($normalized);
                    // Detect duplicates within the file itself (e.g. same email appearing twice).
                    $duplicateKey = $profile->inFileDuplicateKey($normalized);
                    if ($duplicateKey !== null && $duplicateKey !== '') {
                        if (isset($seenRowKeys[$duplicateKey])) {
                            $skipped++;
                            $this->incrementErrorCount($errorCounts, 'duplicate_in_file');
                            $this->pushError($errors, $lineNumber, $identifier, 'duplicate_in_file', null);
                            return;
                        }
                        $seenRowKeys[$duplicateKey] = true;
                    }

                    $result = $commit ? $profile->commitRow($normalized, $context) : $profile->dryRunRow($normalized, $context);
                    $status = (string) $result['status'];
                    $code = isset($result['code']) ? (string) $result['code'] : '';
                    $message = isset($result['message']) ? (string) $result['message'] : null;

                    if ($status === 'created') {
                        $created++;
                        return;
                    }
                    if ($status === 'would_create') {
                        $wouldCreate++;
                        return;
                    }
                    if ($status === 'skipped') {
                        $skipped++;
                        $errorCode = $code !== '' ? $code : 'email_exists';
                        $this->incrementErrorCount($errorCounts, $errorCode);
                        $this->pushError($errors, $lineNumber, $identifier, $errorCode, $message);
                        return;
                    }

                    $failed++;
                    $errorCode = $code !== '' ? $code : 'unexpected_error';
                    $this->incrementErrorCount($errorCounts, $errorCode);
                    $this->pushError($errors, $lineNumber, $identifier, $errorCode, $message);
                } catch (\Throwable $exception) {
                    $failed++;
                    $this->incrementErrorCount($errorCounts, 'unexpected_error');
                    if ($identifier === '' && $mapped) {
                        try {
                            $identifier = $profile->rowIdentifier($mapped);
                        } catch (\Throwable $innerException) {
                            $identifier = '';
                        }
                    }
                    $this->pushError($errors, $lineNumber, $identifier, 'unexpected_error', null);
                }
            }
        );

        if ($commit) {
            return [
                'ok' => true,
                'processed' => $processed,
                'created' => $created,
                'skipped' => $skipped,
                'failed' => $failed,
                'error_counts' => $errorCounts,
                'errors' => $errors,
            ];
        }

        return [
            'ok' => true,
            'rows_total' => $processed,
            'valid_rows' => $wouldCreate + $skipped,
            'would_create' => $wouldCreate,
            'would_skip' => $skipped,
            'would_fail' => $failed,
            'errors' => $errors,
        ];
    }

    /**
     * @param array<int, string> $headers
     * @return array<string, mixed>
     */
    private function normalizeMapping(array $mappingInput, array $headers, ImportProfileInterface $profile): array
    {
        $mappingRaw = $mappingInput['mapping'] ?? $mappingInput;
        if (!is_array($mappingRaw)) {
            $mappingRaw = [];
        }

        $allowedTargets = array_keys($profile->allowedTargets());
        $allowedMap = array_fill_keys($allowedTargets, true);
        $requiredTargets = $profile->requiredTargets();
        $requiredMap = array_fill_keys($requiredTargets, true);
        $mapping = [];
        $mappedTargets = [];

        foreach ($headers as $header) {
            $target = trim((string) ($mappingRaw[$header] ?? ''));
            if ($target === '' || !isset($allowedMap[$target])) {
                continue;
            }
            if (isset($mappedTargets[$target])) {
                continue;
            }
            $mapping[$header] = $target;
            $mappedTargets[$target] = true;
        }

        foreach ($requiredMap as $target => $_) {
            if (!isset($mappedTargets[$target])) {
                return ['ok' => false, 'code' => 'mapping_required_missing'];
            }
        }

        return [
            'ok' => true,
            'mapping' => $mapping,
            'mapped_targets' => array_keys($mappedTargets),
        ];
    }

    /**
     * @param array<string, string> $mapping
     * @param array<string, string> $rowAssoc
     * @return array<string, string>
     */
    private function mapRow(array $rowAssoc, array $mapping): array
    {
        $mapped = [];
        foreach ($mapping as $header => $target) {
            if (isset($mapped[$target])) {
                continue;
            }
            $mapped[$target] = trim((string) ($rowAssoc[$header] ?? ''));
        }
        return $mapped;
    }

    /**
     * @param array<int, string> $headers
     * @return array<string, string>
     */
    private function buildAutoMapping(array $headers, ImportProfileInterface $profile): array
    {
        $mapping = [];
        $aliases = $profile->autoMapAliases();
        $normalizedAliasMap = [];

        foreach ($aliases as $target => $targetAliases) {
            $normalizedAliasMap[$this->normalizeHeader($target)] = $target;
            foreach ($targetAliases as $alias) {
                $normalizedAliasMap[$this->normalizeHeader($alias)] = $target;
            }
        }

        $assignedTargets = [];
        foreach ($headers as $header) {
            $normalized = $this->normalizeHeader($header);
            if ($normalized === '') {
                continue;
            }
            $target = $normalizedAliasMap[$normalized] ?? '';
            if ($target === '' || isset($assignedTargets[$target])) {
                continue;
            }
            $mapping[$header] = $target;
            $assignedTargets[$target] = true;
        }

        return $mapping;
    }

    /**
     * @param array<int, string> $mappedTargets
     */
    private function usesAssignmentTargets(array $mappedTargets): bool
    {
        foreach ($mappedTargets as $target) {
            if (in_array($target, ['tenant', 'role', 'department'], true)) {
                return true;
            }
        }
        return false;
    }

    private function canImportAssignments(int $userId): bool
    {
        return $this->permissionService->userHas($userId, PermissionService::USERS_IMPORT_ASSIGNMENTS);
    }

    /**
     * @return array<string, mixed>
     */
    private function buildContext(int $currentUserId): array
    {
        $isGlobalAdmin = $this->userScopeGateway->hasGlobalAccess($currentUserId);
        return [
            'current_user_id' => $currentUserId,
            'is_global_admin' => $isGlobalAdmin,
            // Empty allowed_tenant_ids signals "no restriction" to profile validators for global admins.
            'allowed_tenant_ids' => $isGlobalAdmin ? [] : $this->userScopeGateway->getUserTenantIds($currentUserId),
            'default_tenant_id' => $this->settingsDefaultsGateway->getDefaultTenantId(),
            'default_role_id' => $this->settingsDefaultsGateway->getDefaultRoleId(),
            'default_department_id' => $this->settingsDefaultsGateway->getDefaultDepartmentId(),
            'can_import_assignments' => $this->canImportAssignments($currentUserId),
        ];
    }

    private function currentTenantId(): ?int
    {
        $tenant = $this->sessionStore->get('current_tenant', []);
        if (!is_array($tenant)) {
            return null;
        }

        $tenantId = (int) ($tenant['id'] ?? 0);
        return $tenantId > 0 ? $tenantId : null;
    }

    // Fallback to uniqid if random_bytes is unavailable — not cryptographically ideal but only
    // used for session-scoped state tokens, not for security-critical values.
    private function newToken(): string
    {
        try {
            return bin2hex(random_bytes(24));
        } catch (\Throwable $exception) {
            return str_replace('.', '', uniqid('import_', true));
        }
    }

    private function normalizeHeader(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }

        $value = $this->lower($value);
        $value = str_replace(['-', '.', ' '], '_', $value);
        $value = preg_replace('/[^a-z0-9_]/', '', $value) ?? '';
        $value = preg_replace('/_+/', '_', $value) ?? '';
        return trim($value, '_');
    }

    /**
     * @param array<int, array{row_number:int,identifier:string,code:string,message:string}> $errors
     */
    // Cap the error list to avoid oversized responses — error_counts still reflects the full count.
    private function pushError(array &$errors, int $lineNumber, string $identifier, string $code, ?string $message): void
    {
        if (count($errors) >= self::MAX_ERROR_ROWS) {
            return;
        }
        $errors[] = [
            'row_number' => $lineNumber,
            'identifier' => $identifier,
            'code' => $code,
            'message' => $message !== null && $message !== '' ? $message : $this->defaultMessageForCode($code),
        ];
    }

    /**
     * @param array<string, int> $errorCounts
     */
    private function incrementErrorCount(array &$errorCounts, string $code): void
    {
        $code = trim($code);
        if ($code === '') {
            return;
        }
        $errorCounts[$code] = (int) ($errorCounts[$code] ?? 0) + 1;
    }

    private function defaultMessageForCode(string $code): string
    {
        $map = [
            'upload_missing' => $this->translate('No upload file provided'),
            'upload_too_large' => $this->translate('Upload exceeds the maximum size'),
            'invalid_file_type' => $this->translate('Invalid file type'),
            'invalid_csv_header' => $this->translate('CSV header is invalid'),
            'empty_csv' => $this->translate('CSV file has no data rows'),
            'max_rows_exceeded' => $this->translate('CSV row limit exceeded'),
            'mapping_required_missing' => $this->translate('Required mapping is missing'),
            'assignment_permission_required' => $this->translate('Assignment import permission is required'),
            'invalid_email' => $this->translate('Email is invalid'),
            'duplicate_in_file' => $this->translate('Duplicate entry in CSV file'),
            'email_exists' => $this->translate('Email already exists'),
            'invalid_locale' => $this->translate('Locale is invalid'),
            'invalid_active' => $this->translate('Active value is invalid'),
            'invalid_hire_date' => $this->translate('Hire date must be YYYY-MM-DD'),
            'invalid_tenant_uuid' => $this->translate('Tenant must be a valid UUID'),
            'tenant_not_found' => $this->translate('Tenant is missing, inactive or invalid'),
            'role_not_found' => $this->translate('Role is missing, inactive or invalid'),
            'department_not_found' => $this->translate('Department is missing, inactive or invalid'),
            'department_tenant_mismatch' => $this->translate('Department does not belong to tenant'),
            'assignment_out_of_scope' => $this->translate('Tenant assignment is out of your scope'),
            'code_exists' => $this->translate('Code already exists'),
            'department_exists' => $this->translate('Department already exists for this tenant'),
            'create_failed' => $this->translate('Entry could not be created'),
            'unexpected_error' => $this->translate('Unexpected error during import'),
        ];
        return $map[$code] ?? $this->translate('Unexpected error during import');
    }

    public function messageForCode(string $code): string
    {
        return $this->defaultMessageForCode(trim($code));
    }

    private function lower(string $value): string
    {
        if (function_exists('mb_strtolower')) {
            return mb_strtolower($value, 'UTF-8');
        }
        return strtolower($value);
    }

}
