<?php

namespace App\Services;

use App\Models\ConfigurationModel;
use App\Models\SchoolYearClosingBatchModel;
use App\Models\SchoolYearModel;
use App\Models\SchoolYearTransitionLogModel;
use App\Support\SchoolYear\SchoolYearStatus;
use CodeIgniter\Database\BaseConnection;
use InvalidArgumentException;
use RuntimeException;

final class SchoolYearManagementService
{
    public function __construct(
        private readonly SchoolYearModel $schoolYearModel,
        private readonly ConfigurationModel $configurationModel,
        private readonly SchoolYearTransitionLogModel $transitionLogModel,
        private readonly SchoolYearClosingBatchModel $closingBatchModel,
        private readonly SchoolYearValidationService $validationService,
        private readonly BaseConnection $db,
    ) {
    }

    public function createDraft(array $payload, ?int $userId = null): int
    {
        $payload = $this->metadataPayload($payload);
        $payload['status'] = SchoolYearStatus::DRAFT;
        $payload['created_by'] = $userId;
        $payload['updated_by'] = $userId;

        $this->validationService->validateMetadata($payload);

        $this->db->transStart();
        $id = $this->schoolYearModel->insert($payload, true);
        if ($id !== false) {
            $this->log((int) $id, null, SchoolYearStatus::DRAFT, 'create', $userId);
        }
        $this->db->transComplete();

        if ($id === false || $this->db->transStatus() === false) {
            throw new RuntimeException($this->firstModelError('Unable to create school year.'));
        }

        return (int) $id;
    }

    public function updateMetadata(int $id, array $payload, ?int $userId = null): void
    {
        $year = $this->requireYear($id);
        $status = (string) $year['status'];

        if (SchoolYearStatus::isReadonly($status) || $status === SchoolYearStatus::CLOSING) {
            throw new InvalidArgumentException('This school year is read-only and cannot be edited.');
        }

        $payload = $this->metadataPayload($payload);
        $payload['updated_by'] = $userId;
        $this->validationService->validateMetadata($payload, $id);

        $this->db->transStart();
        $updated = $this->schoolYearModel->update($id, $payload);
        if ($updated !== false) {
            $this->log($id, $status, $status, 'metadata_update', $userId);
        }
        $this->db->transComplete();

        if ($updated === false || $this->db->transStatus() === false) {
            throw new RuntimeException($this->firstModelError('Unable to update school year.'));
        }
    }

    public function activate(int $id, ?int $userId = null): void
    {
        $year = $this->requireYear($id);
        $from = (string) $year['status'];

        if (! SchoolYearStatus::canTransition($from, SchoolYearStatus::ACTIVE)) {
            throw new InvalidArgumentException('Only draft or approved reopened school years can be activated.');
        }

        $this->db->transStart();
        $activeYears = $this->schoolYearModel->where('status', SchoolYearStatus::ACTIVE)->findAll();
        $now = date('Y-m-d H:i:s');

        foreach ($activeYears as $activeYear) {
            if ((int) $activeYear['id'] === $id) {
                continue;
            }

            $this->schoolYearModel->update((int) $activeYear['id'], [
                'status' => SchoolYearStatus::CLOSING,
                'closing_started_at' => $now,
                'updated_by' => $userId,
            ]);
            $this->log((int) $activeYear['id'], SchoolYearStatus::ACTIVE, SchoolYearStatus::CLOSING, 'activation_displaced_active_year', $userId, [
                'activated_school_year_id' => $id,
            ]);
        }

        $this->schoolYearModel->update($id, [
            'status' => SchoolYearStatus::ACTIVE,
            'activated_at' => $now,
            'updated_by' => $userId,
        ]);
        $this->configurationModel->setConfigValueByKey('school_year', (string) $year['name']);
        $this->syncActiveYearSession((string) $year['name']);
        $this->log($id, $from, SchoolYearStatus::ACTIVE, 'activate', $userId);

        $this->db->transComplete();

        if ($this->db->transStatus() === false) {
            throw new RuntimeException('Unable to activate school year.');
        }
    }

    public function deleteDraft(int $id, ?int $userId = null): void
    {
        $year = $this->requireYear($id);
        if (($year['status'] ?? '') !== SchoolYearStatus::DRAFT) {
            throw new InvalidArgumentException('Only unused draft school years can be deleted. Archive historical years instead.');
        }

        if ($this->hasDependentRecords($id, (string) $year['name'])) {
            throw new InvalidArgumentException('This school year cannot be deleted because related data exists. Archive historical years instead.');
        }

        $this->db->transStart();
        $this->log($id, SchoolYearStatus::DRAFT, null, 'delete_draft', $userId);
        $this->schoolYearModel->delete($id);
        $this->db->transComplete();

        if ($this->db->transStatus() === false) {
            throw new RuntimeException('Unable to delete draft school year.');
        }
    }

    public function archive(int $id, ?int $userId = null): void
    {
        $this->transition($id, SchoolYearStatus::ARCHIVED, 'archive', $userId, [
            'archived_at' => date('Y-m-d H:i:s'),
        ]);
    }

    public function reopen(int $id, string $reason, ?int $userId = null): void
    {
        if (trim($reason) === '') {
            throw new InvalidArgumentException('A reopen reason is required.');
        }

        $this->transition($id, SchoolYearStatus::ACTIVE, 'reopen', $userId, [
            'metadata' => ['reason' => trim($reason)],
        ]);
    }

    public function latestTransitionByYear(): array
    {
        if (! $this->db->tableExists('school_year_transition_logs')) {
            return [];
        }

        $rows = $this->transitionLogModel
            ->orderBy('created_at', 'DESC')
            ->findAll();
        $latest = [];

        foreach ($rows as $row) {
            $yearId = (int) ($row['school_year_id'] ?? 0);
            if ($yearId > 0 && ! isset($latest[$yearId])) {
                $latest[$yearId] = $row;
            }
        }

        return $latest;
    }

    public function log(int $schoolYearId, ?string $from, ?string $to, string $action, ?int $userId = null, array $metadata = []): void
    {
        if (! $this->db->tableExists('school_year_transition_logs')) {
            throw new RuntimeException('School year lifecycle tables are missing. Run database migrations before changing school-year status.');
        }

        $this->transitionLogModel->insert([
            'school_year_id' => $schoolYearId,
            'from_status' => $from,
            'to_status' => $to,
            'action' => $action,
            'performed_by' => $userId,
            'metadata_json' => $metadata !== [] ? json_encode($metadata, JSON_UNESCAPED_SLASHES) : null,
            'created_at' => date('Y-m-d H:i:s'),
        ]);
    }

    private function transition(int $id, string $to, string $action, ?int $userId, array $options = []): void
    {
        $year = $this->requireYear($id);
        $from = (string) $year['status'];

        if (! SchoolYearStatus::canTransition($from, $to)) {
            throw new InvalidArgumentException("Cannot transition school year from {$from} to {$to}.");
        }

        if ($to === SchoolYearStatus::ARCHIVED && ! $this->hasFinalizedClosingBatch($id)) {
            throw new InvalidArgumentException('A school year can be archived only after a finalized closing batch exists.');
        }

        if ($to === SchoolYearStatus::ACTIVE) {
            $otherActive = $this->schoolYearModel
                ->where('status', SchoolYearStatus::ACTIVE)
                ->where('id !=', $id)
                ->first();
            if ($otherActive !== null) {
                throw new InvalidArgumentException('Another school year is already active. Activate or close years through the controlled lifecycle first.');
            }
        }

        $this->db->transStart();
        $data = [
            'status' => $to,
            'updated_by' => $userId,
        ];

        foreach (['archived_at', 'closed_at', 'closing_started_at'] as $field) {
            if (isset($options[$field])) {
                $data[$field] = $options[$field];
            }
        }

        $this->schoolYearModel->update($id, $data);
        $this->log($id, $from, $to, $action, $userId, $options['metadata'] ?? []);
        $this->db->transComplete();

        if ($this->db->transStatus() === false) {
            throw new RuntimeException('Unable to update school year status.');
        }
    }

    private function requireYear(int $id): array
    {
        $year = $this->schoolYearModel->find($id);
        if ($year === null) {
            throw new InvalidArgumentException('School year not found.');
        }

        return $year;
    }

    private function metadataPayload(array $payload): array
    {
        return [
            'name' => trim((string) ($payload['name'] ?? '')),
            'starts_on' => $this->nullableDate($payload['starts_on'] ?? null),
            'ends_on' => $this->nullableDate($payload['ends_on'] ?? null),
            'description' => trim((string) ($payload['description'] ?? '')) ?: null,
            'registration_starts_on' => $this->nullableDate($payload['registration_starts_on'] ?? null),
            'registration_ends_on' => $this->nullableDate($payload['registration_ends_on'] ?? null),
            'previous_school_year_id' => $this->nullableInt($payload['previous_school_year_id'] ?? null),
        ];
    }

    private function nullableDate(mixed $value): ?string
    {
        $value = trim((string) $value);

        return $value !== '' ? $value : null;
    }

    private function nullableInt(mixed $value): ?int
    {
        return is_numeric($value) && (int) $value > 0 ? (int) $value : null;
    }

    private function hasDependentRecords(int $id, string $name): bool
    {
        if (
            $this->db->tableExists('school_year_closing_batches')
            && $this->closingBatchModel->where('source_school_year_id', $id)->orWhere('target_school_year_id', $id)->first() !== null
        ) {
            return true;
        }

        foreach (['invoices', 'payments', 'student_class', 'teacher_class', 'calendar_events', 'events'] as $table) {
            if ($this->db->tableExists($table) && $this->db->fieldExists('school_year', $table)) {
                $count = $this->db->table($table)->where('school_year', $name)->countAllResults();
                if ($count > 0) {
                    return true;
                }
            }
        }

        return false;
    }

    private function hasFinalizedClosingBatch(int $sourceSchoolYearId): bool
    {
        if (! $this->db->tableExists('school_year_closing_batches')) {
            return false;
        }

        return $this->closingBatchModel
            ->where('source_school_year_id', $sourceSchoolYearId)
            ->whereIn('status', ['completed', 'closed'])
            ->first() !== null;
    }

    private function firstModelError(string $fallback): string
    {
        $errors = $this->schoolYearModel->errors();
        $first = reset($errors);

        return is_string($first) && $first !== '' ? $first : $fallback;
    }

    private function syncActiveYearSession(string $schoolYearName): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return;
        }

        session()->set('school_year', $schoolYearName);
        session()->remove('selected_school_year_id');
        session()->remove('selected_school_year');
    }
}
