<?php

declare(strict_types=1);

namespace PpPhp\Modules\Mobile\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\DB;
use PpPhp\Core\Security;
use PpPhp\Core\ProjectNav;
use PpPhp\Core\ModuleState;
use PpPhp\Core\ProjectLocator;
use PpPhp\Core\ProjectDatabaseProfile;

/**
 * Mobile — configures the native mobile-app experience baked into the generated
 * app. Settings persist to config/mobile.json (ModuleState) and are read by the
 * build (BuildEngine → BuildWriter::injectMobileStylesheet / generatePwaAssets),
 * which emits window.PP_MOBILE so app-mobile.js toggles/renders each feature at
 * runtime. A custom app icon uploads to config/mobile-icon.<ext>.
 */
class MobileController
{
    private array $project = [];
    private string $slug = '';

    /** Allowed tab-bar active-icon effects (kept in sync with app-mobile.css). */
    private const EFFECTS = ['none', 'pop', 'bounce', 'pulse', 'glow', 'wiggle'];

    public function __construct(
        private Config $config,
    ) {}

    public function index(string $slug): string
    {
        $this->initProject($slug);
        $project = $this->project;

        $navItems  = ProjectNav::items('/mobile');
        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = $_SESSION['user'];
        $pageTitle = 'Mobile';

        $state       = ModuleState::load($slug, 'mobile');
        $config      = $this->mergeDefaults(is_array($state) ? $state : []);
        $configJson  = json_encode($config, JSON_UNESCAPED_SLASHES);
        $pageOptions = json_encode($this->pageOptions($slug), JSON_UNESCAPED_SLASHES);
        $effects     = json_encode(self::EFFECTS);
        $iconUrl     = $config['pwa']['iconFile'] !== ''
            ? rtrim((string) $baseUrl, '/') . '/project/' . rawurlencode($slug) . '/mobile/icon?t=' . time()
            : '';
        // Best-guess URL for the built app (owner can override in the QR panel).
        $scheme      = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host        = (string) ($_SERVER['HTTP_HOST'] ?? 'localhost');
        $appUrlGuess = $scheme . '://' . $host . '/';

        ob_start();
        include __DIR__ . '/../Views/index.php';
        return ob_get_clean();
    }

    public function save(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();

        $input   = json_decode((string) file_get_contents('php://input'), true) ?? $_POST;
        $section = (string) ($input['section'] ?? '');
        $data    = $input['data'] ?? null;
        if ($section === '' || !is_array($data)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'Section and data are required.']);
            exit;
        }

        $state = ModuleState::load($slug, 'mobile');
        if (!is_array($state)) $state = [];
        if ($section === 'all') {
            // Preserve the uploaded icon filename (managed by the upload endpoint,
            // not sent by the settings form) across a wholesale save.
            $keepIcon = (string) ($state['pwa']['iconFile'] ?? '');
            $state = $data;
            if (!isset($state['pwa']) || !is_array($state['pwa'])) $state['pwa'] = [];
            if (empty($state['pwa']['iconFile'])) $state['pwa']['iconFile'] = $keepIcon;
        } else {
            $state[$section] = $data;
        }
        $state = $this->mergeDefaults($state);
        ModuleState::save($slug, 'mobile', $state);

        echo json_encode(['success' => true, 'message' => 'Mobile settings saved.', 'config' => $state]);
        exit;
    }

    public function load(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $state = ModuleState::load($slug, 'mobile');
        echo json_encode([
            'success'     => true,
            'config'      => $this->mergeDefaults(is_array($state) ? $state : []),
            'pageOptions' => $this->pageOptions($slug),
        ]);
        exit;
    }

    /** Upload a custom app icon (PNG / SVG / JPG). Stored in the project config dir. */
    public function uploadIcon(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();

        $f = $_FILES['icon'] ?? null;
        if (!$f || ($f['error'] ?? 1) !== UPLOAD_ERR_OK || !is_uploaded_file($f['tmp_name'])) {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'No file uploaded.']);
            exit;
        }
        if (($f['size'] ?? 0) > 1048576) {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'Icon must be 1 MB or smaller.']);
            exit;
        }
        $mime = function_exists('mime_content_type') ? (string) @mime_content_type($f['tmp_name']) : (string) ($f['type'] ?? '');
        $extByMime = ['image/png' => 'png', 'image/svg+xml' => 'svg', 'image/jpeg' => 'jpg', 'image/webp' => 'webp'];
        $ext = $extByMime[$mime] ?? '';
        if ($ext === '') {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'Use a PNG, SVG, JPG or WebP image.']);
            exit;
        }

        $dir = ProjectLocator::dir($slug) . '/config';
        if (!is_dir($dir)) @mkdir($dir, 0775, true);
        // Remove any previous icon of a different extension so only one exists.
        foreach (['png', 'svg', 'jpg', 'webp'] as $e) { @unlink($dir . '/mobile-icon.' . $e); }
        $fname = 'mobile-icon.' . $ext;
        if (!@move_uploaded_file($f['tmp_name'], $dir . '/' . $fname)) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Could not save the icon.']);
            exit;
        }

        $state = ModuleState::load($slug, 'mobile');
        if (!is_array($state)) $state = [];
        $state = $this->mergeDefaults($state);
        $state['pwa']['iconFile'] = $fname;
        ModuleState::save($slug, 'mobile', $state);

        $url = rtrim((string) $this->config->get('base_url'), '/') . '/project/' . rawurlencode($slug) . '/mobile/icon?t=' . time();
        echo json_encode(['success' => true, 'message' => 'Icon uploaded.', 'url' => $url]);
        exit;
    }

    /** Remove the custom app icon (build falls back to the auto-generated one). */
    public function removeIcon(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $dir = ProjectLocator::dir($slug) . '/config';
        foreach (['png', 'svg', 'jpg', 'webp'] as $e) { @unlink($dir . '/mobile-icon.' . $e); }
        $state = ModuleState::load($slug, 'mobile');
        if (is_array($state)) { $state = $this->mergeDefaults($state); $state['pwa']['iconFile'] = ''; ModuleState::save($slug, 'mobile', $state); }
        echo json_encode(['success' => true, 'message' => 'Icon removed.']);
        exit;
    }

    /** Generate a fresh VAPID key pair server-side (no external tool needed). */
    public function generateVapid(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        try {
            require_once constant('BASE_PATH') . '/core/Push/WebPush.php';
            $keys = \PpWebPush::generateVapidKeys();
            echo json_encode(['success' => true, 'publicKey' => $keys['publicKey'], 'privateKey' => $keys['privateKey']]);
        } catch (\Throwable $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Could not generate keys: ' . $e->getMessage()]);
        }
        exit;
    }

    /** Stream the stored icon for the settings-page preview. */
    public function icon(string $slug): void
    {
        $this->initProject($slug);
        $state = ModuleState::load($slug, 'mobile');
        $file  = (string) (($state['pwa']['iconFile'] ?? '') ?: '');
        $path  = $file !== '' ? ProjectLocator::dir($slug) . '/config/' . basename($file) : '';
        if ($path === '' || !is_file($path)) { http_response_code(404); exit; }
        $types = ['png' => 'image/png', 'svg' => 'image/svg+xml', 'jpg' => 'image/jpeg', 'webp' => 'image/webp'];
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        header('Content-Type: ' . ($types[$ext] ?? 'application/octet-stream'));
        header('Cache-Control: no-store');
        readfile($path);
        exit;
    }

    /** Link targets a custom tab can point at: the project's tables + dashboard. */
    private function pageOptions(string $slug): array
    {
        $opts = [['label' => 'Dashboard', 'link' => '/generated/dashboard/index.php']];
        if (ProjectDatabaseProfile::isStandaloneSlug($slug)) return $opts;
        $sel = (array) (ModuleState::load($slug, 'tables')['selectedTables'] ?? []);
        try {
            if (empty($sel)) {
                $rows = DB::Query('SHOW TABLES')->fetchAll(\PDO::FETCH_NUM);
                $sel = array_map(static fn ($r) => $r[0], $rows);
            }
        } catch (\Throwable $e) { /* files-only fallback */ }
        foreach ($sel as $t) {
            $t = (string) $t;
            if ($t === '') continue;
            $opts[] = ['label' => ucwords(str_replace('_', ' ', $t)), 'link' => '/generated/' . $t . '/list.php'];
        }
        return $opts;
    }

    /** Coerce any stored/posted state into the full known schema (safe defaults). */
    private function mergeDefaults(array $in): array
    {
        $d = $this->defaultConfig();
        $bool = static fn ($v, $fallback) => is_bool($v) ? $v : (is_int($v) ? (bool) $v : (is_string($v) ? in_array(strtolower($v), ['1', 'true', 'yes', 'on'], true) : $fallback));

        $out = ['enabled' => $bool($in['enabled'] ?? null, $d['enabled'])];
        $out['darkMode'] = in_array(($in['darkMode'] ?? ''), ['off', 'auto', 'light', 'dark'], true) ? $in['darkMode'] : $d['darkMode'];

        // Per-component flavour matrix.
        $flvIn = is_array($in['flavours'] ?? null) ? $in['flavours'] : [];
        $out['flavours'] = [
            'list'    => in_array(($flvIn['list'] ?? ''), ['auto', 'cards', 'table'], true) ? $flvIn['list'] : $d['flavours']['list'],
            'dates'   => in_array(($flvIn['dates'] ?? ''), ['custom', 'native'], true) ? $flvIn['dates'] : $d['flavours']['dates'],
            'selects' => in_array(($flvIn['selects'] ?? ''), ['native', 'sheet'], true) ? $flvIn['selects'] : $d['flavours']['selects'],
            'numbers' => in_array(($flvIn['numbers'] ?? ''), ['plain', 'stepper'], true) ? $flvIn['numbers'] : $d['flavours']['numbers'],
        ];
        $out['previewUrl'] = mb_substr(trim((string) ($in['previewUrl'] ?? '')), 0, 300);

        $out['features'] = [];
        foreach ($d['features'] as $k => $def) $out['features'][$k] = $bool($in['features'][$k] ?? null, $def);

        // Tab bar
        $tIn = is_array($in['tabbar'] ?? null) ? $in['tabbar'] : [];
        $mode = in_array(($tIn['mode'] ?? ''), ['auto', 'custom'], true) ? $tIn['mode'] : $d['tabbar']['mode'];
        $count = (int) ($tIn['count'] ?? $d['tabbar']['count']);
        $count = max(2, min(5, $count));
        $effect = in_array(($tIn['effect'] ?? ''), self::EFFECTS, true) ? $tIn['effect'] : 'none';
        $items = [];
        foreach ((array) ($tIn['items'] ?? []) as $it) {
            if (!is_array($it)) continue;
            $label = mb_substr(trim((string) ($it['label'] ?? '')), 0, 20);
            $icon  = preg_replace('/[^a-z0-9-]/', '', strtolower((string) ($it['icon'] ?? 'bi-dot'))) ?: 'bi-dot';
            $link  = mb_substr(trim((string) ($it['link'] ?? '')), 0, 300);
            if ($label === '' && $link === '') continue;
            $items[] = ['label' => $label ?: 'Tab', 'icon' => $icon, 'link' => $link, 'enabled' => $bool($it['enabled'] ?? null, true)];
            if (count($items) >= 5) break;
        }
        $out['tabbar'] = [
            'mode'       => $mode,
            'count'      => $count,
            'showLabels' => $bool($tIn['showLabels'] ?? null, $d['tabbar']['showLabels']),
            'effect'     => $effect,
            'items'      => $items,
        ];

        // Edge (Back/Forward) buttons appearance
        $eIn = is_array($in['edgeNav'] ?? null) ? $in['edgeNav'] : [];
        $hexv = static function ($c) { $c = trim((string) $c); return ($c !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $c)) ? $c : ''; };
        $op = is_numeric($eIn['opacity'] ?? null) ? (float) $eIn['opacity'] : 0.5;
        $out['edgeNav'] = [
            'size'      => max(40, min(80, (int) ($eIn['size'] ?? 52))),
            'bg'        => $hexv($eIn['bg'] ?? ''),
            'iconColor' => $hexv($eIn['iconColor'] ?? ''),
            'opacity'   => max(0.2, min(1.0, $op)),
        ];

        // PWA
        $pIn = is_array($in['pwa'] ?? null) ? $in['pwa'] : [];
        $color = trim((string) ($pIn['themeColor'] ?? ''));
        if ($color !== '' && !preg_match('/^#[0-9a-fA-F]{3,8}$/', $color)) $color = '';
        $iconFile = preg_replace('/[^a-zA-Z0-9._-]/', '', (string) ($pIn['iconFile'] ?? ''));
        $out['pwa'] = [
            'enabled'    => $bool($pIn['enabled'] ?? null, $d['pwa']['enabled']),
            'appName'    => mb_substr(trim((string) ($pIn['appName'] ?? '')), 0, 60),
            'themeColor' => $color,
            'iconFile'   => $iconFile,
        ];

        // Web Push (VAPID) — keys are base64url; only stored, never executed here.
        $pushIn = is_array($in['push'] ?? null) ? $in['push'] : [];
        $out['push'] = [
            'vapidPublic'  => preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($pushIn['vapidPublic'] ?? '')),
            'vapidPrivate' => preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($pushIn['vapidPrivate'] ?? '')),
            'subject'      => mb_substr(trim((string) ($pushIn['subject'] ?? '')), 0, 120),
        ];
        return $out;
    }

    private function defaultConfig(): array
    {
        return [
            'enabled'  => true,
            'darkMode' => 'off',
            'features' => [
                // shell + navigation
                'tabbar'         => true,
                'edgeNav'        => true,
                'edgeSwipe'      => true,
                'swipeActions'   => true,
                'skeleton'       => true,
                'haptics'        => true,
                'pageTransition' => true,
                // Phase A — component mobilisation
                'inputEnhance'   => true,
                'nativePickers'  => true,
                'cardify'        => true,
                'liveSearch'     => true,
                'stickyHeaders'  => true,
                'emptyStates'    => true,
                'toasts'         => true,
                'sheets'         => true,
                // Phase B — signature interactions
                'pullRefresh'    => true,
                'infiniteScroll' => false,
                'modalDetail'    => false,
                'viewTransitions' => true,
                'kpiCarousel'    => true,
                // Phase C — platform depth
                'offline'        => false,
                'push'           => false,
                'shareTarget'    => false,
                'installPrompt'  => true,
                'appBadge'       => true,
                'formDrafts'     => true,
            ],
            'tabbar' => [
                'mode'       => 'auto',
                'count'      => 4,
                'showLabels' => true,
                'effect'     => 'none',
                'items'      => [],
            ],
            'edgeNav' => [
                'size'      => 52,
                'bg'        => '',
                'iconColor' => '',
                'opacity'   => 0.5,
            ],
            'pwa' => [
                'enabled'    => true,
                'appName'    => '',
                'themeColor' => '',
                'iconFile'   => '',
            ],
            'push' => [
                'vapidPublic'  => '',
                'vapidPrivate' => '',
                'subject'      => '',
            ],
            'flavours' => [
                'list'    => 'auto',
                'dates'   => 'custom',
                'selects' => 'native',
                'numbers' => 'plain',
            ],
            'previewUrl' => '',
        ];
    }

    private function initProject(string $slug): void
    {
        Security::RequireAuth();
        $this->slug = $slug;

        DB::Init();
        $project = DB::Select('projects', ['slug' => $slug, 'created_by' => Security::CurrentUserData()['id']]);
        if (empty($project) && Security::IsAdmin()) {
            $project = DB::Select('projects', ['slug' => $slug]);
        }
        if (empty($project)) {
            http_response_code(404);
            echo 'Project not found.';
            exit;
        }
        $this->project = $project[0];

        if (ProjectDatabaseProfile::isStandaloneSlug($slug)) {
            return;
        }
        DB::Init($slug);
        DB::SetConnection($slug);
    }
}
