<?php

namespace App\Core;

/**
 * BaseController — HTTP Response Dispatcher
 *
 * All application controllers extend this class. It provides
 * the four core response methods: render, redirect, json, flash.
 *
 * Architecture rules (DBP v1.0 Ch.4, SAD v1.1 Ch.6):
 *   - Controllers contain NO business logic.
 *   - Controllers receive a Request and call a Service.
 *   - Controllers render a view or redirect based on the Service result.
 *   - Controllers are thin — every public method should be ≤ 15 lines.
 *
 * Lifecycle (per request):
 *   Router → Controller::method(Request) → Service → render()/redirect()/json()
 *
 * View rendering:
 *   Views are stored in app/views/{path}.php (shared) or within a module.
 *   Layout wrapping: app/views/layouts/app.php receives $content (view HTML).
 *   If no layout file exists, the view content is output directly.
 *
 * Example usage in a concrete controller:
 *   class GuestListController extends BaseController {
 *       public function index(Request $request): void {
 *           $service = new GuestService();
 *           $guests  = $service->getGuestList($request->param('workspace_id'));
 *           $this->render('couple/guests/index', ['guests' => $guests]);
 *       }
 *   }
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.4, SAD v1.1 Ch.6, INIT-008
 */
abstract class BaseController
{
    /**
     * The default layout file to wrap all rendered views.
     * Relative to ROOT_PATH/app/views/layouts/.
     * Module controllers may override this for portal-specific layouts.
     * @var string
     */
    protected string $layout = 'app';

    /**
     * Base constructor.
     *
     * PHP 8.0+ requires an explicit constructor on abstract classes when child
     * controllers call parent::__construct(). Without this explicit no-op
     * constructor, PHP throws "Cannot call constructor".
     *
     * All child controller constructors call parent::__construct() so this
     * declaration must remain present.
     *
     * @ref PHP 8.0 migration — abstract class constructor requirement
     */
    public function __construct()
    {
        // No-op: intentionally empty.
        // Child controllers call parent::__construct() to satisfy PHP 8.0+.
    }


    // ─────────────────────────────────────────────────────────────────────────
    // RENDER — HTML VIEW RESPONSE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Render a PHP view file, optionally wrapped in the application layout.
     *
     * View path format: 'couple/dashboard' → app/views/couple/dashboard.php
     * Layout path     : app/views/layouts/{$this->layout}.php
     *
     * Data variables are extracted into the view scope.
     * Flash messages from the session are automatically available as $flash.
     *
     * If the view file does not exist, a 404 is rendered.
     * If the layout file does not exist, the view is output without wrapping.
     *
     * @param string               $view  View path relative to app/views/ (no .php).
     * @param array<string, mixed> $data  Variables made available inside the view.
     * @return void
     */
    protected function render(string $view, array $data = []): void
    {
        $viewPath = ROOT_PATH . '/app/views/' . ltrim($view, '/') . '.php';

        if (!file_exists($viewPath)) {
            Logger::error('View file not found', ['view' => $view, 'path' => $viewPath]);
            ErrorHandler::renderNotFound();
            return;
        }

        // Pull flash messages for the view, then clear them.
        $flash = $this->pullFlash();

        // Extract data variables into view scope.
        // Also expose $flash so views can display notifications.
        $data['flash'] = $flash;
        extract($data, EXTR_SKIP); // EXTR_SKIP: never overwrite existing variables.

        // Capture view output into $content.
        ob_start();
        require $viewPath;
        $content = (string) ob_get_clean();

        // Wrap in layout if available.
        $layoutPath = ROOT_PATH . '/app/views/layouts/' . $this->layout . '.php';

        ob_start();
        if (file_exists($layoutPath)) {
            // The layout has access to $content, $data, and $flash.
            require $layoutPath;
        } else {
            // No layout — output view content directly.
            echo $content;
        }
        $output = (string) ob_get_clean();

        // Rewrite URLs to include base path if necessary
        $base = base_path();
        if ($base !== '') {
            $pattern = '#(href|src|action)="/(?!/)([^"]*)"#i';
            $output = preg_replace_callback($pattern, function ($matches) use ($base) {
                $attr = $matches[1];
                $path = $matches[2];
                $trimmedBase = ltrim($base, '/');
                
                // If path already starts with the base path prefix, do not prepend it again.
                if ($path === $trimmedBase || strpos($path, $trimmedBase . '/') === 0) {
                    return $attr . '="/' . $path . '"';
                }
                return $attr . '="' . $base . '/' . $path . '"';
            }, $output);
        }

        echo $output;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // REDIRECT
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Send an HTTP redirect response and terminate execution.
     *
     * Inveetaire only issues 302 (Found) redirects.
     * For post-login redirect or post-action redirect, always use 302.
     *
     * $path may be:
     *   - An absolute path:  '/login'
     *   - A full URL:        'http://localhost/inveetaire/public/login'
     *
     * @param string $path        The redirect destination.
     * @param int    $statusCode  HTTP status code (default 302 Found).
     * @return void
     */
    protected function redirect(string $path, int $statusCode = 302): void
    {
        // Resolve dynamic URL
        $url = url($path);

        // Prevent response splitting: strip newlines from the Location value.
        $safe = str_replace(["\r", "\n"], '', $url);

        http_response_code($statusCode);
        header('Location: ' . $safe);
        exit;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // JSON — API RESPONSE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Send a JSON response and terminate execution.
     *
     * All JSON responses use a consistent envelope structure:
     *   {
     *     "status":  "success" | "error",
     *     "data":    { ... } | null,
     *     "message": "Human-readable description" | null
     *   }
     *
     * When $code < 400: status = 'success'
     * When $code ≥ 400: status = 'error'
     *
     * Usage:
     *   $this->json(['guest_id' => 42]);             // 200 success
     *   $this->json([], 422, 'Validation failed');   // 422 error
     *
     * @param array<string, mixed> $data     Response payload.
     * @param int                  $code     HTTP status code (default 200).
     * @param string|null          $message  Optional human-readable message.
     * @return void
     */
    protected function json(array $data = [], int $code = 200, ?string $message = null): void
    {
        $status = ($code < 400) ? 'success' : 'error';

        $envelope = [
            'status'  => $status,
            'data'    => empty($data) ? null : $data,
            'message' => $message,
        ];

        http_response_code($code);
        header('Content-Type: application/json; charset=UTF-8');

        // JSON_UNESCAPED_UNICODE: prevent \uXXXX encoding of multi-byte chars.
        // JSON_UNESCAPED_SLASHES: cleaner URLs in JSON responses.
        echo json_encode($envelope, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // FLASH MESSAGES
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Store a flash message in the session for the next request.
     *
     * Flash messages are displayed once then removed (pulled in the next render).
     * Requires an active session (Session::start() — INIT-010).
     *
     * Flash types (convention):
     *   'success' — green notification
     *   'error'   — red notification
     *   'warning' — amber notification
     *   'info'    — blue notification
     *
     * @param string $type    Message type: 'success', 'error', 'warning', 'info'.
     * @param string $message Human-readable message text.
     * @return void
     */
    protected function flash(string $type, string $message): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            // Session not started (before INIT-010). Log a warning and continue.
            Logger::warning('flash() called before session is active', [
                'type'    => $type,
                'message' => $message,
            ]);
            return;
        }

        $_SESSION['flash'][$type] = $message;
    }

    /**
     * Retrieve and clear all flash messages from the session.
     * Called automatically by render() before extracting view data.
     *
     * @return array<string, string>  Type => message pairs, or empty array.
     */
    private function pullFlash(): array
    {
        if (session_status() !== PHP_SESSION_ACTIVE || !isset($_SESSION['flash'])) {
            return [];
        }

        $flash = $_SESSION['flash'];
        unset($_SESSION['flash']);

        return is_array($flash) ? $flash : [];
    }

    // ─────────────────────────────────────────────────────────────────────────
    // VIEW HELPERS (available for subclasses)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Send a 404 Not Found response and terminate.
     * Convenience wrapper for controllers that need to abort with a 404.
     *
     * @return void
     */
    protected function notFound(): void
    {
        ErrorHandler::renderNotFound();
    }

    /**
     * Send a 403 Forbidden response and terminate.
     * Convenience wrapper for controllers that need to abort with a 403.
     *
     * @return void
     */
    protected function forbidden(): void
    {
        ErrorHandler::renderForbidden();
    }
}
