<?php

namespace app\controllers;

use app\models\ApkFile;
use Exception;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\web\NotFoundHttpException;
use \yii\helpers\Url;

class ApkController extends Controller
{
    /**
     * @var string Versão semântica atual da API (MAJOR.MINOR.PATCH).
     * Utilizada pelo frontend para validar compatibilidade de funcionalidades.
     */
    const API_VERSION = "1.2.0";

    /**
     * @var string Versão mínima da API que o cliente deve suportar para garantir 
     * que não existam quebras de contrato (Breaking Changes).
     */
    const API_MIN_COMPATIBLE = "1.0.0";

    /**
     * @var string
     */
    private $apkPath = "";

    public function init()
    {
        parent::init();
        $this->apkPath = Yii::$app->params["apkFolder"];
    }

    /**
     * Evento executado antes de qualquer action deste controller.
     * 
     * Este método prepara a resposta final e injeta headers customizados de versão.
     * 
     * A vantagem desta abordagem é que o frontend (Android/Retrofit) pode validar a 
     * compatibilidade da API em qualquer requisição, sem necessidade de chamadas extras.
     *
     * @param \yii\base\Action $action A action que foi executada.
     * @return mixed                    O resultado processado com os novos headers.
     */
    public function beforeAction($action)
    {
        $headers = Yii::$app->response->headers;
        $headers->set('X-API-Version', self::API_VERSION);
        $headers->set('X-API-Min-Compatible', self::API_MIN_COMPATIBLE);

        return parent::beforeAction($action);
    }

    public function actionIndex()
    {
        $response = Yii::$app->response;

        $response->format = Response::FORMAT_JSON;

        $response->data = [
            "message" => "VisionSoft APK API",
            "api_version" => self::API_VERSION,
            "min_compatible" => self::API_MIN_COMPATIBLE,
            "system_warnings" => $this->getSystemWarnings(),
            "endpoints" => [
                [
                    "description" => "List of available APKs",
                    "request" => "GET /list"
                ],
                [
                    "description" => "Download APK",
                    "request" => "GET /download/<filename>"
                ],
                [
                    "description" => "Get detailed APK information",
                    "request" => "GET /info/<filename>"
                ],
                [
                    "description" => "Get latest version of the apk updater",
                    "request" => "GET /updater"
                ]
            ]
        ];

        if (empty($response->data['system_warnings']))
            unset($response->data['system_warnings']);

        return $response->send();
    }

    public function actionList()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $apkFiles = [];
        $errors = [];

        if (!is_dir($this->apkPath)) {
            return [
                'error' => 'APK directory not found',
                'path' => $this->apkPath
            ];
        }

        $files = scandir($this->apkPath);

        foreach ($files as $file) {
            if (pathinfo($file, PATHINFO_EXTENSION) !== 'apk') {
                continue;
            }

            $filePath = "{$this->apkPath}/{$file}";
            $apk = new ApkFile($file, $filePath);


            if ($apk->loadMetadata()) {
                if ($this->isApkUpdaterOnlyFile($file)) {
                    continue;
                }

                $apkFiles[] = [
                    "application_label" => $apk->applicationLabel,
                    "version_name" => $apk->versionName,
                    "filename" => $apk->filename,
                    "file_size_bytes" => $apk->fileSize,
                    "file_size_human" => $this->formatBytes($apk->fileSize),
                    "download_url" => Url::to(["/download/{$file}"], true)
                ];
            }
        }

        $response = $apkFiles;

        if (!empty($errors)) {
            $response['parsing_errors'] = $errors;
        }

        return $response;
    }

    /**
     * Gets a unique list of application names (labels) for client-side filtering.
     * This avoids sending redundant metadata when only names are needed.
     *
     * @return array
     */
    public function actionLabels()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        if (!is_dir($this->apkPath)) {
            return ['error' => 'APK directory not found'];
        }
        if (!is_dir($this->apkPath)) {
            return ['error' => 'APK directory not found'];
        }

        $files = scandir($this->apkPath);
        $uniqueLabels = [];
        $files = scandir($this->apkPath);
        $uniqueLabels = [];

        foreach ($files as $file) {
            if (pathinfo($file, PATHINFO_EXTENSION) !== 'apk' || $this->isApkUpdaterOnlyFile($file)) {
                continue;
            }
            foreach ($files as $file) {
                if (pathinfo($file, PATHINFO_EXTENSION) !== 'apk' || $this->isApkUpdaterOnlyFile($file)) {
                    continue;
                }

                $filePath = "{$this->apkPath}/{$file}";
                $apk = new ApkFile($file, $filePath);

                if ($apk->loadMetadata()) {
                    $label = $apk->packageName;

                    if (!empty($label) && !isset($uniqueLabels[$label])) {
                        $uniqueLabels[$label] = $label;
                    }
                }
            }
            $filePath = "{$this->apkPath}/{$file}";
            $apk = new ApkFile($file, $filePath);

            if ($apk->loadMetadata()) {
                $label = $apk->packageName;

                if (!empty($label) && !isset($uniqueLabels[$label])) {
                    $uniqueLabels[$label] = $label;
                }
            }
        }

        // Return as a simple indexed list: ["App Alpha", "App Beta"]
        return array_values($uniqueLabels);
    }

    public function actionInfo($filename)
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $filePath = "{$this->apkPath}/{$filename}";

        if (!file_exists($filePath) || pathinfo($filename, PATHINFO_EXTENSION) !== 'apk') {
            throw new NotFoundHttpException("APK file '{$filename}' not found.");
        }

        $apk = new ApkFile($filename, $filePath);
        if (!$apk->loadMetadata()) {
            return [
                'error' => $apk->getParseError()
            ];
        }

        return [
            "filename" => $apk->filename,
            "package_name" => $apk->packageName,
            "version_code" => $apk->versionCode,
            "version_name" => $apk->versionName,
            "application_label" => $apk->applicationLabel,
            "sdk" => [
                "min" => $apk->minSdkVersion,
                "target" => $apk->targetSdkVersion
            ],
            "size" => [
                "file_bytes" => $apk->fileSize,
                "installed_bytes" => $apk->installedSize,
                "file_human" => $this->formatBytes($apk->fileSize),
                "installed_human" => $this->formatBytes($apk->installedSize),
            ],
            "permissions" => $apk->permissions,
            "permissions_count" => count($apk->permissions),
            "download_url" => Url::to(["/download/{$filename}"], true)
        ];
    }

    /**
     * Procura o ficheiro no disco, e prepara, processa a envia o ficheiro forçando o download
     *
     * @param mixed $filename
     * @throws NotFoundHttpException
     * @return Response
     */
    public function actionDownload($filename): Response
    {
        $response = Yii::$app->response;
        $response->format = Response::FORMAT_RAW;

        try {
            $filePath = "{$this->apkPath}/{$filename}";

            if (!file_exists($filePath) || pathinfo($filename, PATHINFO_EXTENSION) !== 'apk') {
                throw new NotFoundHttpException("APK file '{$filename}' not found.");
            }

            return $response->sendFile(
                $filePath,
                $filename,
                ['mimeType' => 'application/vnd.android.package-archive']
            );

        } catch (Exception $e) {
            $response = Yii::$app->response->setStatusCodeByException($e);
            $response->data = [
                "message" => $e->getMessage()
            ];
            return $response;
        }
    }

    /**
     * Get the latest version of an APK by application name
     * Returns only the latest version with download URL
     *
     * @return Response
     * @throws NotFoundHttpException
     */
    public function actionUpdater(): Response
    {
        $response = Yii::$app->response;
        $response->format = Response::FORMAT_JSON;

        if (!is_dir($this->apkPath)) {
            $response->data = [
                'error' => 'APK directory not found',
                'path' => $this->apkPath
            ];
            return $response;
        }

        $files = scandir($this->apkPath);
        $matchingApks = [];

        // Normalize search term for comparison
        $searchTerm = strtolower(trim("APK Updater"));

        foreach ($files as $file) {
            if (pathinfo($file, PATHINFO_EXTENSION) !== 'apk') {
                continue;
            }

            $filePath = "{$this->apkPath}/{$file}";
            $apk = new ApkFile($file, $filePath);


            if (!$apk->loadMetadata()) {
                continue;
            }

            // Match by application label (case-insensitive)
            $appLabel = strtolower(trim($apk->applicationLabel ?? ''));


            if ($appLabel === $searchTerm || strpos($appLabel, $searchTerm) !== false) {
                $matchingApks[] = [
                    'apk' => $apk,
                    'versionCode' => (int) $apk->versionCode,
                    'filename' => $file
                ];
            }
        }

        if (empty($matchingApks)) {
            throw new NotFoundHttpException("No APK found with for the APK Updater.");
        }

        // Sort by version code (descending) to get latest
        usort($matchingApks, function ($a, $b) {
            return $b['versionCode'] - $a['versionCode'];
        });

        // Get the latest version
        $latestApk = $matchingApks[0]['apk'];
        $latestVersion = $latestApk->versionName;
        // if ($version === $latestVersion) {
        //     $response->statusCode = 204;
        // } else {
        $latestFilename = $matchingApks[0]['filename'];
        $latestFilename = $matchingApks[0]['filename'];

        $response->data = [
            "latestVersion" => $latestVersion,
            "download_url" => Url::to(["/download/{$latestFilename}"], true)
        ];
        $response->data = [
            "latestVersion" => $latestVersion,
            "download_url" => Url::to(["/download/{$latestFilename}"], true)
        ];
        // }

        return $response;
    }

    /**
     * Check if file should only appear in /update endpoint
     * Files with naming pattern containing "ApkUpdater" are excluded from /list
     *
     * @param string $filename
     * @return bool
     */
    private function isApkUpdaterOnlyFile(string $filename): bool
    {
        return strpos($filename, 'ApkUpdater') !== false;
    }

    /**
     * Converts a PHP shorthand memory notation string into its equivalent value in bytes.
     *
     * PHP ini values such as `memory_limit` use a shorthand suffix to represent unit multipliers:
     *   - K / k = Kilobytes  (× 1,024)
     *   - M / m = Megabytes  (× 1,048,576)
     *   - G / g = Gigabytes  (× 1,073,741,824)
     *
     * If no recognised suffix is present the raw integer value is returned as-is (plain bytes).
     * The special value "-1", returned by `ini_get('memory_limit')` when memory is unlimited,
     * is passed through unchanged so callers can detect it explicitly.
     *
     * Examples:
     *   "512K"      → 524,288
     *   "256M"      → 268,435,456
     *   "2G"        → 2,147,483,648
     *   "134217728" → 134,217,728  (no suffix = plain bytes)
     *   "-1"        → -1           (unlimited, PHP sentinel value)
     *
     * @param  string $value  Raw ini value, e.g. "2048M" or "2G".
     * @return float            Equivalent value in bytes, or -1 for unlimited. (Becayse WAMP32bits #no-comments)
     */
    private function toBytes(string $value): float
    {
        $value = trim($value);
        $unit = strtoupper(substr($value, -1));
        $num = (float) $value;

        switch ($unit) {
            case 'G':
                return $num * 1024 ** 3;
            case 'M':
                return $num * 1024 ** 2;
            case 'K':
                return $num * 1024;
            default:
                return $num;
        }
    }

    /**
     * Audits critical PHP runtime settings and returns a list of actionable warnings.
     *
     * Called on every request to `actionIndex()` so that operators are immediately
     * aware of mis-configured environments without needing to inspect the server directly.
     * The warnings array is omitted from the JSON response entirely when it is empty,
     * keeping the output clean in correctly configured environments.
     *
     * Checks performed
     * ─────────────────────────────────────────────────────────────────────────────
     * 1. memory_limit
     *    Minimum recommended : 2048M (2G)
     *    Rationale           : APK parsing and metadata extraction can load large
     *                          binary files into memory. Insufficient memory causes
     *                          silent failures or fatal OOM errors.
     *    Special case        : -1 (unlimited) is always accepted without warning.
     *
     * 2. max_execution_time
     *    Minimum recommended : 540 seconds (9 minutes)
     *    Rationale           : Bulk APK scanning (actionList) may iterate over many
     *                          large files. Below 540 s the process risks a premature
     *                          timeout mid-scan.
     *    Special case        : 0 (unlimited) is always accepted without warning.
     *
     * Each warning entry contains:
     *   - type    : ini directive name   (e.g. "memory_limit")
     *   - current : value read at runtime (e.g. "128M")
     *   - minimum : the expected threshold (e.g. "2048M")
     *   - message : human-readable explanation for logs / dashboards
     *
     * @return array<int, array{type: string, current: string, minimum: string, message: string}>
     *         Empty array when all settings meet the recommended thresholds.
     */
    private function getSystemWarnings(): array
    {
        $warnings = [];

        // ── memory_limit ────────────────────────────────────────────────────────
        $memoryRaw = ini_get('memory_limit');
        $memoryBytes = $this->toBytes($memoryRaw);
        $minMemory = $this->toBytes('2048M');

        // -1 = unlimited (acceptable); anything below 2048M triggers a warning
        if ($memoryBytes !== -1 && $memoryBytes < $minMemory) {
            $warnings[] = [
                'type' => 'memory_limit',
                'current' => $memoryRaw,
                'minimum' => '2048M',
                'message' => "memory_limit ({$memoryRaw}) is below the recommended minimum of 2048M.",
            ];
        }

        // ── max_execution_time ──────────────────────────────────────────────────
        $execTime = (int) ini_get('max_execution_time');
        $minExecTime = 120;

        // 0 = unlimited (acceptable); anything below 540s triggers a warning
        if ($execTime !== 0 && $execTime < $minExecTime) {
            $warnings[] = [
                'type' => 'max_execution_time',
                'current' => "{$execTime}s",
                'minimum' => "{$minExecTime}s",
                'message' => "max_execution_time ({$execTime}s) is below the recommended minimum of {$minExecTime}s.",
            ];
        }

        return $warnings;
    }

    /**
     * Formats a raw byte count into a human-readable string (B, KB, MB, GB).
     *
     * Uses 1024-based (binary) units, consistent with how Android reports
     * storage consumption on-device.
     *
     * @param  int    $bytes     Raw byte count.
     * @param  int    $precision Decimal places (default: 2).
     * @return string            e.g. "47.32 MB"
     */
    private function formatBytes(int $bytes, int $precision = 2): string
    {
        if ($bytes <= 0) {
            return '0 B';
        }

        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $pow = min((int) floor(log($bytes, 1024)), count($units) - 1);

        return round($bytes / (1024 ** $pow), $precision) . ' ' . $units[$pow];
    }
}
