<?php

namespace App\Controllers\View;

use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;

class FilesController extends Controller
{
    public function receipt(string $name)
    {
        // 1) Path traversal guard
        if ($name !== basename($name)) {
            return $this->response->setStatusCode(400, 'Invalid filename');
        }

        // 2) Allow-list extensions (optional but safer)
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
        if (!in_array($ext, $allowed, true)) {
            throw PageNotFoundException::forPageNotFound();
        }

        $expense = $this->expenseRecordForFile($name);
        if ($expense === null || !$this->canViewExpenseFile($expense)) {
            return $this->response->setStatusCode(403, 'You are not allowed to access this file.');
        }

        $path = WRITEPATH . 'uploads/receipts/' . $name;
        if (!is_file($path)) {
            throw PageNotFoundException::forPageNotFound();
        }

        // 4) MIME detection (finfo is more reliable than mime_content_type)
        $mime = 'application/octet-stream';
        if (function_exists('finfo_open')) {
            $fi = finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) {
                $detected = finfo_file($fi, $path);
                if ($detected) $mime = $detected;
                finfo_close($fi);
            }
        }

        // 5) Caching (ETag + Last-Modified) and 304 support
        $mtime = filemtime($path) ?: time();
        $etag  = md5($name . '|' . $mtime . '|' . filesize($path));

        $ifNoneMatch     = $this->request->getHeaderLine('If-None-Match');
        $ifModifiedSince = $this->request->getHeaderLine('If-Modified-Since');

        if ($ifNoneMatch === $etag || (strtotime($ifModifiedSince) >= $mtime)) {
            return $this->response
                ->setStatusCode(304)
                ->setHeader('ETag', $etag)
                ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
        }

        // 6) Stream the file inline
        return $this->response
            ->setStatusCode(200)
            ->setHeader('Content-Type', $mime)
            ->setHeader('Content-Disposition', 'inline; filename="' . $name . '"')
            ->setHeader('Content-Length', (string) filesize($path))
            ->setHeader('ETag', $etag)
            ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT')
            ->setHeader('Cache-Control', 'public, max-age=86400')
            ->setBody(file_get_contents($path));
    }

    public function reimb(string $name)
    {
        // 1) Path traversal guard
        if ($name !== basename($name)) {
            return $this->response->setStatusCode(400, 'Invalid filename');
        }

        // 2) Allow-list extensions
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
        if (!in_array($ext, $allowed, true)) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $reimbursement = $this->reimbursementRecordForFile($name);
        if ($reimbursement === null || !$this->canViewReimbursementFile($reimbursement)) {
            return $this->response->setStatusCode(403, 'You are not allowed to access this file.');
        }

        $path = WRITEPATH . 'uploads/reimbursements/' . $name;
        if (!is_file($path)) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        // 4) MIME detection (prefer finfo)
        $mime = 'application/octet-stream';
        if (function_exists('finfo_open')) {
            $fi = finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) {
                $detected = finfo_file($fi, $path);
                if ($detected) {
                    $mime = $detected;
                }
                finfo_close($fi);
            }
        } elseif (function_exists('mime_content_type')) {
            $mime = mime_content_type($path) ?: $mime;
        }

        // 5) Caching (ETag + Last-Modified) and 304 support
        $mtime = filemtime($path) ?: time();
        $size  = filesize($path) ?: 0;
        $etag  = md5($name . '|' . $mtime . '|' . $size);

        $ifNoneMatch     = trim($this->request->getHeaderLine('If-None-Match'), '"');
        $ifModifiedSince = $this->request->getHeaderLine('If-Modified-Since');
        $imsTime         = $ifModifiedSince ? strtotime($ifModifiedSince) : false;

        if (($ifNoneMatch && $ifNoneMatch === $etag) ||
            ($imsTime !== false && $imsTime >= $mtime)
        ) {
            return $this->response
                ->setStatusCode(304)
                ->setHeader('ETag', $etag)
                ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
        }

        // 6) Stream the file inline
        return $this->response
            ->setStatusCode(200)
            ->setHeader('Content-Type', $mime)
            ->setHeader('X-Content-Type-Options', 'nosniff')
            ->setHeader('Content-Disposition', 'inline; filename="' . $name . '"')
            ->setHeader('Content-Length', (string) $size)
            ->setHeader('ETag', $etag)
            ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT')
            ->setHeader('Cache-Control', 'public, max-age=86400')
            ->setBody(file_get_contents($path));
    }

    public function earlyDismissalSignature(string $name)
    {
        // 1) Path traversal guard
        if ($name !== basename($name)) {
            return $this->response->setStatusCode(400, 'Invalid filename');
        }

        // 2) Allow-list extensions
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
        if (!in_array($ext, $allowed, true)) {
            throw PageNotFoundException::forPageNotFound();
        }

        // 3) Build path under writable (EARLY DISMISSAL SIGNATURES)
        $path = WRITEPATH . 'uploads/early_dismissal_signatures/' . $name;
        if (!is_file($path)) {
            throw PageNotFoundException::forPageNotFound();
        }

        // 4) MIME detection (prefer finfo)
        $mime = 'application/octet-stream';
        if (function_exists('finfo_open')) {
            $fi = finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) {
                $detected = finfo_file($fi, $path);
                if ($detected) {
                    $mime = $detected;
                }
                finfo_close($fi);
            }
        } elseif (function_exists('mime_content_type')) {
            $mime = mime_content_type($path) ?: $mime;
        }

        // 5) Caching (ETag + Last-Modified) and 304 support
        $mtime = filemtime($path) ?: time();
        $size  = filesize($path) ?: 0;
        $etag  = md5($name . '|' . $mtime . '|' . $size);

        $ifNoneMatch     = trim($this->request->getHeaderLine('If-None-Match'), '"');
        $ifModifiedSince = $this->request->getHeaderLine('If-Modified-Since');
        $imsTime         = $ifModifiedSince ? strtotime($ifModifiedSince) : false;

        if (($ifNoneMatch && $ifNoneMatch === $etag) ||
            ($imsTime !== false && $imsTime >= $mtime)
        ) {
            return $this->response
                ->setStatusCode(304)
                ->setHeader('ETag', $etag)
                ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
        }

        // 6) Stream the file inline
        return $this->response
            ->setStatusCode(200)
            ->setHeader('Content-Type', $mime)
            ->setHeader('X-Content-Type-Options', 'nosniff')
            ->setHeader('Content-Disposition', 'inline; filename="' . $name . '"')
            ->setHeader('Content-Length', (string) $size)
            ->setHeader('ETag', $etag)
            ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT')
            ->setHeader('Cache-Control', 'public, max-age=86400')
            ->setBody(file_get_contents($path));
    }

    public function examDraftTeacher(string $name)
    {
        return $this->serveExamDraftFile($name, 'drafts');
    }

    public function examDraftFinal(string $name)
    {
        return $this->serveExamDraftFile($name, 'finals');
    }

    private function serveExamDraftFile(string $name, string $subdir)
    {
        if ($name !== basename($name)) {
            return $this->response->setStatusCode(400, 'Invalid filename');
        }

        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $allowed = ['doc', 'docx', 'pdf'];
        if (!in_array($ext, $allowed, true)) {
            throw PageNotFoundException::forPageNotFound();
        }

        $path = WRITEPATH . 'uploads/exams/' . trim($subdir, '/') . '/' . $name;
        if (!is_file($path)) {
            throw PageNotFoundException::forPageNotFound();
        }

        if (!$this->canAccessExamDraftFile($name, $subdir)) {
            return $this->response->setStatusCode(403, 'You are not allowed to access this file.');
        }

        $mime = 'application/octet-stream';
        if (function_exists('finfo_open')) {
            $fi = finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) {
                $detected = finfo_file($fi, $path);
                if ($detected) {
                    $mime = $detected;
                }
                finfo_close($fi);
            }
        } elseif (function_exists('mime_content_type')) {
            $mime = mime_content_type($path) ?: $mime;
        }

        $mtime = filemtime($path) ?: time();
        $size  = filesize($path) ?: 0;
        $etag  = md5($name . '|' . $mtime . '|' . $size);

        $ifNoneMatch     = trim($this->request->getHeaderLine('If-None-Match'), '"');
        $ifModifiedSince = $this->request->getHeaderLine('If-Modified-Since');
        $imsTime         = $ifModifiedSince ? strtotime($ifModifiedSince) : false;

        if (($ifNoneMatch && $ifNoneMatch === $etag) ||
            ($imsTime !== false && $imsTime >= $mtime)
        ) {
            return $this->response
                ->setStatusCode(304)
                ->setHeader('ETag', $etag)
                ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
        }

        $downloadName = $this->buildDraftDownloadName($name, $subdir);

        return $this->response
            ->setStatusCode(200)
            ->setHeader('Content-Type', $mime)
            ->setHeader('X-Content-Type-Options', 'nosniff')
            ->setHeader('Content-Disposition', 'inline; filename="' . $downloadName . '.' . $ext . '"')
            ->setHeader('Content-Length', (string) $size)
            ->setHeader('ETag', $etag)
            ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT')
            ->setHeader('Cache-Control', 'public, max-age=86400')
            ->setBody(file_get_contents($path));
    }

    private function buildDraftDownloadName(string $filename, string $subdir): string
    {
        $db = Database::connect();
        $column = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
        $row = $db->table('exam_drafts ed')
            ->select('ed.version, ed.exam_type, ed.class_section_id, cs.class_section_name')
            ->join('classSection cs', 'cs.class_section_id = ed.class_section_id', 'left')
            ->where('ed.' . $column, $filename)
            ->limit(1)
            ->get()
            ->getRowArray();

        $classLabel = trim((string) ($row['class_section_name'] ?? 'Class' . ($row['class_section_id'] ?? '0')));
        $typeLabel = trim((string) ($row['exam_type'] ?? 'Exam'));
        $version = 'v' . max(1, (int) ($row['version'] ?? 1));

        $parts = array_filter([
            $this->slugify($classLabel),
            $this->slugify($typeLabel),
            $version,
        ]);

        return implode('_', $parts);
    }

    private function slugify(string $value): string
    {
        $value = trim($value);
        $value = preg_replace('/[^\p{L}\p{N}]+/u', '_', $value);
        $value = trim($value, '_');
        return $value === '' ? 'Exam' : mb_strtolower($value);
    }

    private function resolveExamDraftFileColumn($db): string
    {
        try {
            $fields = $db->getFieldNames('exam_drafts');
            if (in_array('teacher_file', $fields, true)) {
                return 'teacher_file';
            }
            if (in_array('author_file', $fields, true)) {
                return 'author_file';
            }
        } catch (\Throwable $e) {
            log_message('error', 'FilesController::resolveExamDraftFileColumn error: ' . $e->getMessage());
        }
        return 'teacher_file';
    }

    private function resolveExamDraftAuthorIdColumn($db): string
    {
        try {
            $fields = $db->getFieldNames('exam_drafts');
            if (in_array('teacher_id', $fields, true)) {
                return 'teacher_id';
            }
            if (in_array('author_id', $fields, true)) {
                return 'author_id';
            }
        } catch (\Throwable $e) {
            log_message('error', 'FilesController::resolveExamDraftAuthorIdColumn error: ' . $e->getMessage());
        }

        return 'teacher_id';
    }

    private function canAccessExamDraftFile(string $filename, string $subdir): bool
    {
        $userId = (int) (session()->get('user_id') ?? 0);
        if ($userId <= 0) {
            return false;
        }

        $role = strtolower((string) (session()->get('role') ?? ''));
        if (in_array($role, ['admin', 'administrator', 'principal'], true)) {
            return true;
        }

        $db = Database::connect();
        $fileColumn = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
        $authorIdColumn = $this->resolveExamDraftAuthorIdColumn($db);

        $draft = $db->table('exam_drafts ed')
            ->select('ed.class_section_id, ed.school_year, ed.semester, ed.status, ed.' . $authorIdColumn . ' AS draft_author_id')
            ->where('ed.' . $fileColumn, $filename)
            ->limit(1)
            ->get()
            ->getRowArray();

        if (empty($draft)) {
            return false;
        }

        $authorId = (int) ($draft['draft_author_id'] ?? 0);
        if ($authorId > 0 && $authorId === $userId) {
            return true;
        }

        $classSectionId = (int) ($draft['class_section_id'] ?? 0);
        if ($classSectionId <= 0) {
            return false;
        }

        $status = strtolower(trim((string) ($draft['status'] ?? '')));
        if ($subdir === 'drafts' && $status === 'draft') {
            return false;
        }

        $currentSchoolYear = trim((string) (session()->get('school_year') ?? ''));
        $currentSemester = trim((string) (session()->get('semester') ?? ''));
        $draftSchoolYear = trim((string) ($draft['school_year'] ?? ''));
        $draftSemester = trim((string) ($draft['semester'] ?? ''));

        $hasCurrentAssignment = $db->table('teacher_class')
            ->select('id')
            ->where('teacher_id', $userId)
            ->where('class_section_id', $classSectionId);

        if ($currentSchoolYear !== '') {
            $hasCurrentAssignment->where('school_year', $currentSchoolYear);
        }

        $hasCurrentAssignment = $hasCurrentAssignment->limit(1)->countAllResults() > 0;

        if ($hasCurrentAssignment) {
            if ($subdir === 'finals') {
                return true;
            }

            return $draftSchoolYear === '' || $currentSchoolYear === '' || $draftSchoolYear === $currentSchoolYear;
        }

        $assignmentQuery = $db->table('teacher_class')
            ->select('id')
            ->where('teacher_id', $userId)
            ->where('class_section_id', $classSectionId);

        if ($draftSchoolYear !== '') {
            $assignmentQuery->where('school_year', $draftSchoolYear);
        }

        $hasDraftYearAssignment = $assignmentQuery->limit(1)->countAllResults() > 0;
        if (!$hasDraftYearAssignment) {
            return false;
        }

        if ($subdir === 'finals') {
            return true;
        }

        return $draftSemester === '' || $currentSemester === '' || $draftSemester === $currentSemester;
    }

    private function expenseRecordForFile(string $name): ?array
    {
        return \Config\Database::connect()
            ->table('expenses')
            ->where('receipt_path', $name)
            ->get()
            ->getRowArray();
    }

    private function reimbursementRecordForFile(string $name): ?array
    {
        return \Config\Database::connect()
            ->table('reimbursements')
            ->where('receipt_path', $name)
            ->get()
            ->getRowArray();
    }

    private function canViewExpenseFile(array $expense): bool
    {
        if ($this->hasFinancialStaffAccess()) {
            return true;
        }

        $userId = (int) (session()->get('user_id') ?? 0);

        return $userId > 0 && in_array($userId, [
            (int) ($expense['purchased_by'] ?? 0),
            (int) ($expense['added_by'] ?? 0),
            (int) ($expense['approved_by'] ?? 0),
        ], true);
    }

    private function canViewReimbursementFile(array $reimbursement): bool
    {
        if ($this->hasFinancialStaffAccess()) {
            return true;
        }

        $userId = (int) (session()->get('user_id') ?? 0);

        return $userId > 0 && in_array($userId, [
            (int) ($reimbursement['reimbursed_to'] ?? 0),
            (int) ($reimbursement['approved_by'] ?? 0),
            (int) ($reimbursement['added_by'] ?? 0),
        ], true);
    }

    private function hasFinancialStaffAccess(): bool
    {
        $roles = array_map('strtolower', (array) (session()->get('roles') ?? []));
        $activeRole = strtolower((string) (session()->get('role') ?? ''));
        if ($activeRole !== '' && !in_array($activeRole, $roles, true)) {
            $roles[] = $activeRole;
        }

        foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'] as $role) {
            if (in_array($role, $roles, true)) {
                return true;
            }
        }

        return false;
    }
}
