<?php

namespace MintyPHP\Service\User;

use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;

class UserLifecycleService
{
    private const LOCK_NAME = 'user_lifecycle_maintenance';
    private const BATCH_SIZE = 500;

    public function __construct(
        private readonly UserReadRepositoryInterface $userReadRepository,
        private readonly UserWriteRepositoryInterface $userWriteRepository,
        private readonly UserSettingsGateway $settingsGateway,
        private readonly UserLifecycleAuditInterface $userLifecycleAuditService,
        private readonly DatabaseSessionRepository $databaseSessionRepository
    ) {
    }

    public function run(?int $actorUserId = null): array
    {
        $startedAt = microtime(true);
        $runUuid = RepoQuery::uuidV4();
        $triggerType = ($actorUserId ?? 0) > 0 ? 'manual' : 'cron';
        $deactivateDays = $this->settingsGateway->getUserInactivityDeactivateDays();
        $deleteDays = $this->settingsGateway->getUserInactivityDeleteDays();
        if ($deactivateDays <= 0) {
            $deleteDays = 0;
        }

        // Exclude admins with settings/tenant management rights — they must be deactivated manually.
        $privilegedUserIds = $this->userReadRepository->listPrivilegedUserIdsByPermissionKeys([
            PermissionService::SETTINGS_UPDATE,
            PermissionService::TENANTS_UPDATE,
        ]);

        $result = [
            'ok' => true,
            'locked' => false,
            'error' => null,
            'run_uuid' => $runUuid,
            'deactivated_count' => 0,
            'deleted_count' => 0,
            'skipped_count' => 0,
            'skipped_privileged_count' => count($privilegedUserIds),
            'policy' => [
                'deactivate_days' => $deactivateDays,
                'delete_days' => $deleteDays,
            ],
            'duration_ms' => 0,
        ];

        // Advisory lock prevents concurrent runs (e.g. cron and a manual trigger at the same time).
        if (!$this->acquireLock()) {
            $result['ok'] = false;
            $result['locked'] = true;
            $result['error'] = 'lock_not_acquired';
            $result['duration_ms'] = $this->durationMs($startedAt);
            return $result;
        }

        try {
            if ($deactivateDays > 0) {
                while (true) {
                    $ids = $this->userReadRepository->listIdsForAutoDeactivate($deactivateDays, $privilegedUserIds, self::BATCH_SIZE);
                    if (!$ids) {
                        break;
                    }

                    $usersById = [];
                    foreach ($ids as $id) {
                        $user = $this->userReadRepository->find((int) $id);
                        if ($user) {
                            $usersById[(int) $id] = $user;
                        }
                    }
                    $updated = $this->userWriteRepository->setInactiveByIds(
                        $ids,
                        $actorUserId && $actorUserId > 0 ? $actorUserId : null
                    );
                    if ($updated <= 0) {
                        $result['ok'] = false;
                        $result['error'] = 'deactivate_failed';
                        break;
                    }
                    $result['deactivated_count'] += $updated;
                    $this->userWriteRepository->bumpAuthzVersionByUserIds($ids);
                    foreach ($ids as $id) {
                        if (!isset($usersById[(int) $id])) {
                            continue;
                        }
                        $this->userLifecycleAuditService->logDeactivate(
                            $runUuid,
                            $triggerType,
                            $result['policy'],
                            $actorUserId,
                            $usersById[(int) $id],
                            'success',
                            null
                        );
                    }
                    if (count($ids) < self::BATCH_SIZE) {
                        break;
                    }
                }
            }

            if ($deleteDays > 0) {
                while (true) {
                    $ids = $this->userReadRepository->listIdsForAutoDelete($deleteDays, $privilegedUserIds, self::BATCH_SIZE);
                    if (!$ids) {
                        break;
                    }

                    foreach ($ids as $id) {
                        $user = $this->userReadRepository->find((int) $id);
                        if (!$user) {
                            continue;
                        }

                        $auditId = $this->userLifecycleAuditService->logDeleteWithSnapshot(
                            $runUuid,
                            $triggerType,
                            $result['policy'],
                            $actorUserId,
                            $user
                        );
                        if (!$auditId) {
                            $result['skipped_count']++;
                            $this->userLifecycleAuditService->logDeleteFailure(
                                $runUuid,
                                $triggerType,
                                $result['policy'],
                                $actorUserId,
                                $user,
                                'audit_snapshot_failed'
                            );
                            continue;
                        }

                        $deleted = $this->userWriteRepository->deleteByIds([(int) $id]);
                        if ($deleted <= 0) {
                            $result['skipped_count']++;
                            $this->userLifecycleAuditService->updateStatus((int) $auditId, 'failed', 'delete_failed');
                            continue;
                        }
                        $result['deleted_count'] += $deleted;
                    }
                    if (count($ids) < self::BATCH_SIZE) {
                        break;
                    }
                }
            }
        } catch (\Throwable $exception) {
            $result['ok'] = false;
            $result['error'] = 'unexpected_error';
        } finally {
            $this->releaseLock();
            $result['duration_ms'] = $this->durationMs($startedAt);
            // Record run-trigger state every time the lock was acquired —
            // including no-op runs (0 users processed) — so the cockpit
            // KPI tile reflects every actual execution.
            $this->settingsGateway->recordLifecycleLastRun(
                gmdate('Y-m-d H:i:s'),
                $result['ok'] ? 'success' : 'failed'
            );
        }

        return $result;
    }

    private function acquireLock(): bool
    {
        return $this->databaseSessionRepository->acquireAdvisoryLock(self::LOCK_NAME, 0);
    }

    private function releaseLock(): void
    {
        try {
            $this->databaseSessionRepository->releaseAdvisoryLock(self::LOCK_NAME);
        } catch (\Throwable $exception) {
            // no-op
        }
    }

    private function durationMs(float $startedAt): int
    {
        return (int) max(0, round((microtime(true) - $startedAt) * 1000));
    }
}
