<?php

declare(strict_types=1);

namespace App\Modules\Media;

use App\Core\BaseController;
use App\Core\Request;
use App\Core\Session;
use App\Modules\Invitation\InvitationModel;
use App\Services\MediaService;

/**
 * MediaController — HTTP Handler for the Media Engine (G004)
 *
 * Manages the Media Manager page within the Invitation Builder.
 * Handles upload, delete, and file-serve endpoints via AJAX + JSON responses.
 *
 * Controller is intentionally thin — all business logic lives in MediaService.
 *
 * Routes:
 *   GET  /app/couple/invitation/media          → index()   (Media Manager page)
 *   POST /app/couple/invitation/media/upload   → upload()  (AJAX upload)
 *   POST /app/couple/invitation/media/delete   → delete()  (AJAX delete)
 *   GET  /app/couple/media/serve               → serve()   (Secured file stream)
 *
 * @package Inveetaire
 * @ref     G004
 */
class MediaController extends BaseController
{
    protected string $layout = 'couple';

    private InvitationModel $invitationModel;
    private MediaService $mediaService;

    public function __construct()
    {
        parent::__construct();
        $this->invitationModel = new InvitationModel();
        $this->mediaService    = new MediaService();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MEDIA MANAGER PAGE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Display the Media Manager page.
     *
     * Loads the current invitation and media metadata, then renders the
     * media management UI as a tab within the Invitation Builder.
     *
     * @param Request $request
     * @return void
     */
    public function index(Request $request): void
    {
        $workspaceId = (int) Session::get('active_workspace_id');

        try {
            $invitation = $this->invitationModel->getInvitationByWorkspace($workspaceId);
            if ($invitation === null) {
                throw new \RuntimeException('No invitation found for this workspace.');
            }

            $invitationId = (int) $invitation['id'];
            $media        = $this->invitationModel->getMediaSection($invitationId);
            $workspace    = $this->invitationModel->getWorkspace($workspaceId);
            $coupleCode   = (string) ($workspace['slug'] ?? '');
            $slots        = $this->mediaService->getAllSlots();

            // Build serve URLs for currently stored files
            $mediaUrls = $this->buildMediaUrls($media);

            $this->render('invitation/media', [
                'title'         => 'Media Manager',
                'invitation_id' => $invitationId,
                'media'         => $media,
                'media_urls'    => $mediaUrls,
                'slots'         => $slots,
                'couple_code'   => $coupleCode,
            ]);

        } catch (\Throwable $e) {
            $this->flash('error', 'Failed to load Media Manager: ' . $e->getMessage());
            $this->redirect('/app/couple/invitation/edit');
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // UPLOAD ENDPOINT (AJAX / JSON)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Handle a file upload for a given media slot.
     *
     * Expects multipart/form-data POST with:
     *   - file    : uploaded file
     *   - slot    : slot name (e.g. 'hero', 'gallery')
     *   - _csrf   : CSRF token (validated by CsrfMiddleware)
     *
     * Returns JSON:
     *   { "status": "success", "data": { "path": "...", "url": "..." } }
     *
     * @param Request $request
     * @return void
     */
    public function upload(Request $request): void
    {
        $workspaceId = (int) Session::get('active_workspace_id');

        try {
            $slot = trim((string) $request->post('slot', ''));
            $subSlot = trim((string) $request->post('sub_slot', ''));
            if (empty($slot)) {
                $this->json([], 422, 'Slot name is required.');
                return;
            }

            // Retrieve file from $_FILES
            $file = $_FILES['file'] ?? null;
            if ($file === null || !isset($file['tmp_name'])) {
                $this->json([], 422, 'No file received.');
                return;
            }

            // Load workspace slug for storage path
            $workspace  = $this->invitationModel->getWorkspace($workspaceId);
            $coupleCode = (string) ($workspace['slug'] ?? '');
            if (empty($coupleCode)) {
                $this->json([], 500, 'Workspace not found.');
                return;
            }

            // Load current invitation & media metadata
            $invitation = $this->invitationModel->getInvitationByWorkspace($workspaceId);
            if ($invitation === null) {
                $this->json([], 500, 'Invitation not found.');
                return;
            }
            $invitationId = (int) $invitation['id'];
            $meta         = $this->invitationModel->getMediaSection($invitationId);
            $slotConfig   = $this->mediaService->getSlotConfig($slot);

            // Enforce max_count before uploading
            $currentCount = $this->countSlotFiles($meta, $slot, $subSlot);
            if ($currentCount >= $slotConfig['max_count']) {
                $this->json([], 422, sprintf(
                    '%s already has the maximum of %d file(s). Please delete one first.',
                    $slotConfig['label'],
                    $slotConfig['max_count']
                ));
                return;
            }

            // Delegate upload to MediaService
            $relativePath = $this->mediaService->upload($slot, $file, $coupleCode);

            // Update JSON metadata
            $meta = $this->addToMeta($meta, $slot, $relativePath, $slotConfig, $subSlot);
            $this->invitationModel->saveMediaSection($invitationId, $meta);

            // Build serve URL for the uploaded file
            $serveUrl = $this->mediaService->buildServeUrl($relativePath);

            $this->json([
                'path'     => $relativePath,
                'url'      => $serveUrl,
                'slot'     => $slot,
                'sub_slot' => $subSlot,
                'count'    => $this->countSlotFiles($meta, $slot, $subSlot),
                'max'      => $slotConfig['max_count'],
            ], 200, 'File uploaded successfully.');

        } catch (\InvalidArgumentException $e) {
            $this->json([], 422, $e->getMessage());
        } catch (\RuntimeException $e) {
            $this->json([], 422, $e->getMessage());
        } catch (\Throwable $e) {
            $this->json([], 500, 'Upload failed: ' . $e->getMessage());
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DELETE ENDPOINT (AJAX / JSON)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Handle a file deletion for a given media slot.
     *
     * Expects POST with:
     *   - slot : slot name
     *   - path : relative storage path to delete
     *   - _csrf: CSRF token
     *
     * Returns JSON: { "status": "success", ... }
     *
     * @param Request $request
     * @return void
     */
    public function delete(Request $request): void
    {
        $workspaceId = (int) Session::get('active_workspace_id');

        try {
            $slot         = trim((string) $request->post('slot', ''));
            $subSlot      = trim((string) $request->post('sub_slot', ''));
            $relativePath = trim((string) $request->post('path', ''));

            if (empty($slot) || empty($relativePath)) {
                $this->json([], 422, 'Slot and path are required.');
                return;
            }

            // Validate slot exists
            $slotConfig = $this->mediaService->getSlotConfig($slot);

            // Load workspace slug — verify the path belongs to this workspace
            $workspace  = $this->invitationModel->getWorkspace($workspaceId);
            $coupleCode = (string) ($workspace['slug'] ?? '');

            // Security: confirm the path starts with this couple's code
            if (strpos($relativePath, $coupleCode . '/') !== 0) {
                $this->json([], 403, 'You do not have permission to delete this file.');
                return;
            }

            // Load invitation & media metadata
            $invitation = $this->invitationModel->getInvitationByWorkspace($workspaceId);
            if ($invitation === null) {
                $this->json([], 500, 'Invitation not found.');
                return;
            }
            $invitationId = (int) $invitation['id'];
            $meta         = $this->invitationModel->getMediaSection($invitationId);

            // Remove from metadata first, then delete from disk
            $meta = $this->removeFromMeta($meta, $slot, $relativePath, $subSlot);
            $this->invitationModel->saveMediaSection($invitationId, $meta);
            $this->mediaService->delete($relativePath);

            $this->json([
                'slot'     => $slot,
                'sub_slot' => $subSlot,
                'path'     => $relativePath,
                'count'    => $this->countSlotFiles($meta, $slot, $subSlot),
                'max'      => $slotConfig['max_count'],
            ], 200, 'File deleted successfully.');

        } catch (\InvalidArgumentException $e) {
            $this->json([], 422, $e->getMessage());
        } catch (\Throwable $e) {
            $this->json([], 500, 'Delete failed: ' . $e->getMessage());
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // FILE SERVE ENDPOINT
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Stream a media file to the authenticated couple.
     *
     * Validates workspace ownership before streaming.
     * Files in storage/ are not web-accessible — all access goes through here.
     *
     * GET /app/couple/media/serve?path={relativePath}
     *
     * @param Request $request
     * @return void
     */
    public function serve(Request $request): void
    {
        $workspaceId = (int) Session::get('active_workspace_id');

        try {
            $relativePath = trim((string) $request->get('path', ''));
            if (empty($relativePath)) {
                http_response_code(400);
                echo 'Path required.';
                exit;
            }

            // Verify path belongs to this workspace
            $workspace  = $this->invitationModel->getWorkspace($workspaceId);
            $coupleCode = (string) ($workspace['slug'] ?? '');

            if (strpos($relativePath, $coupleCode . '/') !== 0) {
                http_response_code(403);
                echo 'Access denied.';
                exit;
            }

            $this->mediaService->serveFile($relativePath);

        } catch (\Throwable $e) {
            http_response_code(404);
            echo 'File not found.';
            exit;
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE HELPERS
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Count how many files are currently in a slot's metadata.
     */
    private function countSlotFiles(array $meta, string $slot, string $subSlot = ''): int
    {
        $value = $meta[$slot] ?? null;
        
        if ($subSlot !== '' && is_array($value)) {
            $value = $value[$subSlot] ?? null;
        }

        if (is_array($value)) {
            return count($value);
        }
        return $value !== null ? 1 : 0;
    }

    /**
     * Add a new file path to the appropriate slot in the metadata array.
     * Handles both scalar slots (single file) and array slots (multi-file).
     */
    private function addToMeta(array $meta, string $slot, string $path, array $slotConfig, string $subSlot = ''): array
    {
        if ($subSlot !== '') {
            if (!isset($meta[$slot]) || !is_array($meta[$slot])) {
                $meta[$slot] = [];
            }
            if ($slotConfig['max_count'] === 1) {
                $meta[$slot][$subSlot] = $path;
            } else {
                if (!isset($meta[$slot][$subSlot]) || !is_array($meta[$slot][$subSlot])) {
                    $meta[$slot][$subSlot] = [];
                }
                $meta[$slot][$subSlot][] = $path;
            }
            return $meta;
        }

        if ($slotConfig['max_count'] === 1) {
            // Scalar slot — replace
            $meta[$slot] = $path;
        } else {
            // Array slot — append
            if (!isset($meta[$slot]) || !is_array($meta[$slot])) {
                $meta[$slot] = [];
            }
            $meta[$slot][] = $path;
        }
        return $meta;
    }

    /**
     * Remove a file path from the appropriate slot in the metadata array.
     */
    private function removeFromMeta(array $meta, string $slot, string $path, string $subSlot = ''): array
    {
        if ($subSlot !== '') {
            $value = $meta[$slot][$subSlot] ?? null;
            if (is_array($value)) {
                $meta[$slot][$subSlot] = array_values(array_filter($value, fn($v) => $v !== $path));
            } elseif ($value === $path) {
                unset($meta[$slot][$subSlot]);
                if (empty($meta[$slot])) {
                    $meta[$slot] = null;
                }
            }
            return $meta;
        }

        $value = $meta[$slot] ?? null;

        if (is_array($value)) {
            $meta[$slot] = array_values(array_filter($value, fn($v) => $v !== $path));
        } elseif ($value === $path) {
            $meta[$slot] = null;
        }

        return $meta;
    }

    /**
     * Build an array of serve URLs indexed the same way as the media metadata.
     * Null paths remain null; string paths become serve URLs; arrays map each entry.
     *
     * @param array $media  Media metadata from getMediaSection()
     * @return array        Same structure but with serve URLs
     */
    private function buildMediaUrls(array $media): array
    {
        $urls = [];
        $validSlots = array_keys($this->mediaService->getAllSlots());

        foreach ($media as $key => $value) {
            // Skip non-media configuration flags (e.g. music_enabled, music_autoplay)
            if (!in_array($key, $validSlots, true)) {
                continue;
            }

            if (is_array($value)) {
                // Check if it's an associative array (e.g., desktop/mobile variants)
                if (isset($value['desktop']) || isset($value['mobile'])) {
                    $urls[$key] = [];
                    if (isset($value['desktop'])) {
                        $urls[$key]['desktop'] = is_string($value['desktop']) && $value['desktop'] !== '' 
                            ? $this->mediaService->buildServeUrl($value['desktop']) : null;
                    }
                    if (isset($value['mobile'])) {
                        $urls[$key]['mobile'] = is_string($value['mobile']) && $value['mobile'] !== '' 
                            ? $this->mediaService->buildServeUrl($value['mobile']) : null;
                    }
                } else {
                    $urls[$key] = array_map(
                        fn($p) => (is_string($p) && $p !== '') ? $this->mediaService->buildServeUrl($p) : null,
                        $value
                    );
                }
            } elseif (is_string($value) && $value !== '') {
                $urls[$key] = $this->mediaService->buildServeUrl($value);
            } else {
                $urls[$key] = null;
            }
        }
        return $urls;
    }

    /**
     * Stream a media file to a public unauthenticated guest.
     *
     * Validates that the path belongs to an active workspace with a published invitation.
     *
     * GET /i/media/serve?path={relativePath}
     */
    public function servePublic(Request $request): void
    {
        try {
            $relativePath = trim((string) $request->get('path', ''));
            if (empty($relativePath)) {
                http_response_code(400);
                echo 'Path required.';
                exit;
            }

            // Path must have at least 3 segments: couple-slug/slot/filename
            $parts = explode('/', $relativePath);
            if (count($parts) < 3) {
                http_response_code(400);
                echo 'Invalid path structure.';
                exit;
            }

            $workspaceSlug = $parts[0];

            // Verify active workspace
            $db = \App\Core\Database::getConnection();
            $stmt = $db->prepare('
                SELECT w.id 
                FROM workspaces w
                WHERE w.slug = :slug 
                  AND w.workspace_status = \'active\'
                LIMIT 1
            ');
            $stmt->execute(['slug' => $workspaceSlug]);
            $workspace = $stmt->fetch(\PDO::FETCH_ASSOC);

            if (!$workspace) {
                http_response_code(403);
                echo 'Access denied.';
                exit;
            }

            $this->mediaService->serveFile($relativePath);

        } catch (\Throwable $e) {
            http_response_code(404);
            echo 'File not found.';
            exit;
        }
    }

    /**
     * Save music configuration settings to the media section metadata.
     *
     * POST /app/couple/invitation/media/music-settings
     */
    public function saveMusicSettings(Request $request): void
    {
        $workspaceId = (int) Session::get('active_workspace_id');

        try {
            $invitation = $this->invitationModel->getInvitationByWorkspace($workspaceId);
            if ($invitation === null) {
                $this->json([], 404, 'Invitation not found.');
                return;
            }
            $invitationId = (int) $invitation['id'];

            // Retrieve and sanitize fields
            $enabled        = (bool) $request->post('music_enabled');
            $title          = trim((string) $request->post('music_title', ''));
            $artist         = trim((string) $request->post('music_artist', ''));
            $autoplay       = (bool) $request->post('music_autoplay');
            $loop           = (bool) $request->post('music_loop');
            $minimized      = (bool) $request->post('music_minimized');
            $volume         = max(0, min(100, (int) $request->post('music_volume', 50)));

            // Load existing media meta
            $meta = $this->invitationModel->getMediaSection($invitationId);

            // Update configuration
            $meta['music_enabled']   = $enabled;
            $meta['music_title']     = $title;
            $meta['music_artist']    = $artist;
            $meta['music_autoplay']  = $autoplay;
            $meta['music_loop']      = $loop;
            $meta['music_minimized'] = $minimized;
            $meta['music_volume']    = $volume;

            // Save back
            $this->invitationModel->saveMediaSection($invitationId, $meta);

            $this->json([
                'music_enabled'   => $enabled,
                'music_title'     => $title,
                'music_artist'    => $artist,
                'music_autoplay'  => $autoplay,
                'music_loop'      => $loop,
                'music_minimized' => $minimized,
                'music_volume'    => $volume,
            ], 200, 'Music settings updated successfully.');

        } catch (\Throwable $e) {
            $this->json([], 500, 'Failed to save settings: ' . $e->getMessage());
        }
    }
}
