<?php
namespace Core;

class Controller {
    protected \PDO $db;

    public function __construct() {
        $this->db = \Database::getInstance();
    }

    /**
     * Execute a SELECT query and return all rows.
     */
    protected function dbQuery(string $sql, array $params = []): array {
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    /**
     * Execute a SELECT query and return one row (or null).
     */
    protected function dbQueryOne(string $sql, array $params = []): ?array {
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        $result = $stmt->fetch();
        return $result ?: null;
    }

    /**
     * Execute an INSERT/UPDATE/DELETE query.
     */
    protected function dbExecute(string $sql, array $params = []): bool {
        $stmt = $this->db->prepare($sql);
        return $stmt->execute($params);
    }

    protected function render(string $view, array $data = [], string $layout = 'main'): void {
        $content = View::render($view, $data);
        if (strpos($content, '<!DOCTYPE html>') === false && strpos($content, '<html') === false) {
            echo View::layout($layout, $content, $data);
        } else {
            echo $content;
        }
    }
    
    protected function redirect(string $url, int $code = 302): never {
        // If already absolute, use as-is; otherwise prepend APP_URL
        if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://') && !str_starts_with($url, '//')) {
            $url = APP_URL . '/' . ltrim($url, '/');
        }
        header('Location: ' . $url, true, $code);
        exit;
    }
    
    protected function json(mixed $data, int $status = 200): never {
        header('Content-Type: application/json', true, $status);
        echo json_encode($data);
        exit;
    }
    
    protected function jsonError(string $message, int $status = 400): never {
        $this->json(['error' => $message], $status);
    }
    
    protected function generateCsrf(): string {
        $token = bin2hex(random_bytes(32));
        $_SESSION[CSRF_TOKEN_NAME] = $token;
        return $token;
    }
    
    protected function validateCsrf(): bool {
        $token = $this->getPost(CSRF_TOKEN_NAME) ?? $this->getGet(CSRF_TOKEN_NAME);
        if (!$token || !isset($_SESSION[CSRF_TOKEN_NAME])) {
            return false;
        }
        return hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
    }
    
    protected function setFlash(string $type, string $message): void {
        $_SESSION['flash'][$type] = $message;
    }
    
    protected function getFlash(): array {
        $flash = $_SESSION['flash'] ?? [];
        unset($_SESSION['flash']);
        return $flash;
    }

    /**
     * Stash form input in the session so it survives a redirect-after-error
     * (e.g. failed validation) and can repopulate the form on re-render.
     * Never pass password/secret fields in here.
     */
    protected function setOldInput(array $data): void {
        $_SESSION['old_input'] = $data;
    }

    /**
     * Retrieve and clear input previously stashed with setOldInput().
     */
    protected function getOldInput(): array {
        $old = $_SESSION['old_input'] ?? [];
        unset($_SESSION['old_input']);
        return $old;
    }
    
    protected function requireAuth(): void {
        if (!Auth::check()) {
            $this->redirect(APP_URL . '/login');
        }
        $this->enforceIdleTimeout();
        $this->enforceTrialStatus();
    }

    /**
     * Auto-logout after IDLE_TIMEOUT_SECONDS (5 min) of no authenticated
     * requests. This is a coarse server-side backstop (a background AJAX
     * poll would count as "activity" here) — the precise, user-perceived
     * "5 minutes of no mouse/keyboard input" timeout is enforced client-side
     * in main.php, which proactively navigates to /logout well before this
     * would ever fire on its own. This exists for when JS is unavailable.
     */
    private function enforceIdleTimeout(): void {
        $idleSeconds = Auth::idleSeconds();

        if ($idleSeconds !== null && $idleSeconds > IDLE_TIMEOUT_SECONDS) {
            $userId = Auth::id();
            if ($userId) {
                logAudit('logout_idle', 'auth', 'user', $userId);
            }
            Auth::logout();
            $this->setFlash('warning', 'You were logged out after 5 minutes of inactivity. Please sign in again.');
            $this->redirect(APP_URL . '/login');
        }

        Auth::touchActivity();
    }

    /**
     * If the logged-in user's organization is still on a trial that has
     * expired, log them out (preserving billing context) and send them to
     * the subscription payment page instead of letting the request proceed.
     * Session data is refreshed on every login/payment, so this stays
     * accurate without an extra query on every request.
     *
     * Only the org's own users are gated — contractor_user/supplier_user
     * accounts share the client org's org_id (they're the org's portal
     * guests, not the subscriber), so an expired client trial must never
     * lock out the contractors/suppliers working within it.
     */
    private function enforceTrialStatus(): void {
        if (Auth::isSuperAdmin() || Auth::isContractorUser() || Auth::isSupplierUser()) return;

        $orgId = Auth::orgId();
        if (!$orgId) return;

        $user        = Auth::user();
        $status      = $user['subscription_status'] ?? null;
        $trialEndsAt = $user['trial_ends_at'] ?? null;

        if ($status === 'trial' && $trialEndsAt && strtotime($trialEndsAt) <= time()) {
            Auth::logoutForBilling($orgId, Auth::id());
            $this->redirect(APP_URL . '/billing/pay');
        }
    }

    /**
     * Returns the current user's org id, or null for the platform super-admin
     * (whose account has no org_id) so org-scoped queries can list data across all orgs.
     */
    protected function scopedOrgId(): ?int {
        return Auth::isSuperAdmin() ? null : Auth::orgId();
    }


    protected function requirePermission(string $module, string $action): void {
        $this->requireAuth();
        if (!Auth::hasPermission($module, $action)) {
            $this->setFlash('error', 'You do not have permission to access this resource.');
            $this->redirect(APP_URL . '/dashboard');
        }
    }
    
    protected function requireSuperAdmin(): void {
        $this->requireAuth();
        if (!Auth::isSuperAdmin()) {
            $this->setFlash('error', 'Super admin access required.');
            $this->redirect(APP_URL . '/dashboard');
        }
    }
    
    protected function paginate(int $total, int $perPage = 20, ?int $currentPage = null): array {
        if ($currentPage === null) {
            $currentPage = max(1, (int)$this->getGet('page', 1));
        }
        $pages = (int)ceil($total / $perPage);
        return [
            'total' => $total,
            'perPage' => $perPage,
            'current' => $currentPage,
            'pages' => $pages,
            'offset' => ($currentPage - 1) * $perPage
        ];
    }
    
    protected function getPost(string $key, mixed $default = null): mixed {
        return $_POST[$key] ?? $default;
    }
    
    protected function getGet(string $key, mixed $default = null): mixed {
        return $_GET[$key] ?? $default;
    }
    
    protected function validate(array $data, array $rules): array {
        $errors = [];
        $valid = true;
        foreach ($rules as $field => $ruleStr) {
            $fieldRules = explode('|', $ruleStr);
            $val = $data[$field] ?? null;
            foreach ($fieldRules as $rule) {
                if ($rule === 'required' && empty($val)) {
                    $errors[$field][] = "$field is required.";
                    $valid = false;
                }
                if ($rule === 'email' && !empty($val) && !filter_var($val, FILTER_VALIDATE_EMAIL)) {
                    $errors[$field][] = "Invalid email format.";
                    $valid = false;
                }
            }
        }
        return ['valid' => $valid, 'errors' => $errors];
    }
    
    protected function uploadFile(array $fileInput, string $subDir, array $allowedTypes = []): array|false {
        if (!isset($fileInput['tmp_name']) || empty($fileInput['tmp_name'])) {
            return false;
        }
        if ($fileInput['error'] !== UPLOAD_ERR_OK) {
            return false;
        }
        if ($fileInput['size'] > MAX_FILE_SIZE) {
            return false;
        }
        
        $ext = pathinfo($fileInput['name'], PATHINFO_EXTENSION);
        if (!empty($allowedTypes) && !in_array(strtolower($ext), $allowedTypes)) {
            return false;
        }
        
        $filename = generateUniqueFilename($fileInput['name']);
        $destPath = rtrim(UPLOAD_PATH, '/') . '/' . trim($subDir, '/') . '/';
        
        if (!is_dir($destPath)) {
            mkdir($destPath, 0755, true);
        }
        
        if (move_uploaded_file($fileInput['tmp_name'], $destPath . $filename)) {
            return [
                'path' => trim($subDir, '/') . '/' . $filename,
                'name' => $fileInput['name'],
                'size' => $fileInput['size']
            ];
        }
        return false;
    }
}
