<?php
// +----------------------------------------------------------------------
// | onezan-admin 快速开发的管理平台
// +----------------------------------------------------------------------
// | 官方网址：https://www.onezan.cn
// +----------------------------------------------------------------------
// | onezan团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: 543885307@qq.com
// +----------------------------------------------------------------------
namespace app\service\domain\media;

use app\repository\media\MediaFileRepository;
use app\repository\media\MediaGroupRepository;
use app\service\StorageService;
use Core\service\BaseCoreService;
use app\service\domain\storage\CoreStorageService;
use think\facade\Filesystem;
use think\facade\Log;

/**
 * 媒体服务
 * Class CoreMediaService
 * @package app\service\domain\media
 */
class CoreMediaService extends BaseCoreService
{
    /**
     * 保存上传的媒体文件
     * @param mixed $file
     * @param string $type
     * @param int $groupId
     * @return array
     */
    public function saveUpload($file, string $type, int $groupId): array
    {
        $originalName = '';
        try {
            $originalName = (string)$file->getOriginalName();
        } catch (\Throwable $e) {
            $originalName = '';
        }
        $originalName = $this->normalizeOriginalName($originalName);

        $ext = strtolower((string)pathinfo($originalName, PATHINFO_EXTENSION));
        if ($ext === '') {
            try {
                $ext = strtolower((string)$file->extension());
            } catch (\Throwable $e) {
                $ext = '';
            }
        }

        if ($type === '') {
            $type = $this->inferTypeByExt($ext);
        }
        if (!in_array($type, ['image', 'video', 'file'], true)) {
            throw new \RuntimeException('');
        }

        $allowed = $this->allowedExts($type);
        if ($ext === '' || !in_array($ext, $allowed, true)) {
            throw new \RuntimeException('');
        }

        // MIME magic-byte validation
        $realPath = '';
        try {
            $realPath = (string)$file->getPathname();
        } catch (\Throwable $e) {
            $realPath = '';
        }
        if ($realPath !== '' && file_exists($realPath)) {
            if (!$this->validateMime($realPath, $type)) {
                throw new \RuntimeException('');
            }
        }

        $size = 0;
        try {
            $size = (int)$file->getSize();
        } catch (\Throwable $e) {
            $size = 0;
        }
        if ($size > $this->maxSizeBytes($type)) {
            throw new \RuntimeException('');
        }

        $mime = '';
        try {
            $mime = (string)$file->getMime();
        } catch (\Throwable $e) {
            $mime = '';
        }
        $mime = $this->truncateText($mime, 128);

        $savename = Filesystem::disk('public')->putFile('media/' . $type, $file);
        if (!$savename) {
            throw new \RuntimeException('');
        }

        $normalized = ltrim(str_replace('\\', '/', (string)$savename), '/');

        // 本地物理路径
        $localPhysicalPath = app()->getRootPath() . 'public' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $normalized;


            try {
                $securityResult = $this->app->make(\app\service\domain\security\CoreSecurityFilterService::class)->filterImage($localPhysicalPath);
                if ($securityResult['code'] !== 0) {
                    $this->deletePhysicalFile($normalized);
                    throw new \RuntimeException($securityResult['msg']);
                }
            } catch (\RuntimeException $e) {
                throw $e;
            } catch (\Throwable $e) {
                Log::warning('安全检测异常: ' . $e->getMessage());
                $this->deletePhysicalFile($normalized);
                throw new \RuntimeException('安全检测失败，文件已拒绝');
            }

        $thumbnails = null;
        if ($type === 'image') {
            try {
                $processor = $this->app->make(ImageProcessor::class);

                $compressResult = $processor->compressIfNeeded($localPhysicalPath, $processor->getCompressThresholdKB(), 80);

                if ($compressResult['compressed']) {
                    //
                    $newPhysical = $compressResult['path'];
                    $newNormalized = str_replace(
                        [app()->getRootPath() . 'public' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR],
                        ['/', '/'],
                        str_replace('\\', '/', $newPhysical)
                    );
                    $newNormalized = ltrim($newNormalized, '/');
                    $localPhysicalPath = $newPhysical;
                    $normalized = $newNormalized;
                    $size = $compressResult['size'];
                    $ext = 'webp';
                    $mime = 'image/webp';
                }

                $baseName = pathinfo($normalized, PATHINFO_FILENAME);
                $sizes = $processor->getThumbnailSizes();
                $thumbnails = $processor->generateThumbnails(
                    $localPhysicalPath,
                    dirname($normalized),
                    $baseName,
                    $sizes
                );
            } catch (\Throwable $e) {
                Log::warning('缩略图生成失败: ' . $e->getMessage());
                $thumbnails = null;
            }
        }

        $url = StorageService::toPublicUrl($normalized);

        try {
            $storageService = $this->app->make(CoreStorageService::class);
            if ($storageService->current()->provider() !== 'local') {
                $result = $storageService->upload($localPhysicalPath, $normalized);
                $url = $result['url'];
                if ($thumbnails) {
                    foreach ($thumbnails as $label => $thumbPath) {
                        try {
                            $thumbPhysical = app()->getRootPath() . 'public' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $thumbPath;
                            $storageService->upload($thumbPhysical, $thumbPath);
                        } catch (\Throwable $e) {
                            Log::warning('缩略图云端上传失败: ' . $label);
                        }
                    }
                }
            }
        } catch (\Throwable $e) {
        }

        $now = time();

        try {
            $this->fileRepository()->insert([
                'group_id' => $groupId,
                'type' => $type,
                'original_name' => $originalName,
                'ext' => $ext,
                'mime' => $mime,
                'size' => $size,
                'path' => $normalized,
                'url' => $url,
                'thumbnails' => $thumbnails ? json_encode($thumbnails, JSON_UNESCAPED_SLASHES) : null,
                'create_time' => $now,
                'update_time' => $now,
            ]);
        } catch (\Throwable $e) {
            $this->deletePhysicalFile($normalized);
            //
            if ($thumbnails) {
                foreach ($thumbnails as $thumbPath) {
                    $this->deletePhysicalFile($thumbPath);
                }
            }
            $message = $this->app->isDebug() ? $e->getMessage() : '';
            throw new \RuntimeException($message, 0, $e);
        }

        $result = ['url' => $url, 'name' => $originalName, 'size' => $size, 'type' => $type];
        if ($thumbnails) {
            $result['thumbnails'] = $thumbnails;
        }
        return $result;
    }

    /**
     * 删除媒体文件
     * @param int $id
     * @return array
     */
    public function delete(int $id): array
    {
        if ($id <= 0) {
            return ['code' => 1, 'msg' => ''];
        }

        $row = $this->fileRepository()->findById($id);
        if (!$row) {
            return ['code' => 1, 'msg' => ''];
        }

        try {
            $this->fileRepository()->deleteById($id);
        } catch (\Throwable $e) {
            return ['code' => 1, 'msg' => ''];
        }

        $this->deletePhysicalFile((string)($row['path'] ?? ''));
        $this->deleteThumbnailFiles((string)($row['thumbnails'] ?? ''));
        return ['code' => 0, 'msg' => ''];
    }

    /**
     * 批量删除媒体文件
     * @param array $ids
     * @return array
     */
    public function deleteBatch(array $ids): array
    {
        $clean = [];
        foreach ($ids as $id) {
            if (is_numeric($id) && (int)$id > 0) {
                $clean[] = (int)$id;
            }
        }
        $clean = array_values(array_unique($clean));
        if (!$clean) {
            return ['code' => 1, 'msg' => ''];
        }

        $rows = [];
        try {
            $rows = $this->fileRepository()->findByIds($clean);
            $this->fileRepository()->deleteByIds($clean);
        } catch (\Throwable $e) {
            return ['code' => 1, 'msg' => ''];
        }

        foreach ($rows as $row) {
            $this->deletePhysicalFile((string)($row['path'] ?? ''));
            $this->deleteThumbnailFiles((string)($row['thumbnails'] ?? ''));
        }

        return ['code' => 0, 'msg' => ''];
    }

    /**
     * 保存媒体文件分组
     * @param array $payload
     * @return array
     */
    public function saveGroup(array $payload): array
    {
        $id = (int)($payload['id'] ?? 0);
        $name = trim((string)($payload['name'] ?? ''));
        $sort = (int)($payload['sort'] ?? 0);

        if ($name === '') {
            return ['code' => 1, 'msg' => ''];
        }

        $now = time();
        if ($id > 0) {
            if (!$this->groupRepository()->findById($id)) {
                return ['code' => 1, 'msg' => ''];
            }

            $this->groupRepository()->updateById($id, [
                'name' => $name,
                'sort' => $sort,
                'update_time' => $now,
            ]);

            return ['code' => 0, 'msg' => '', 'data' => ['id' => $id]];
        }

        $newId = $this->groupRepository()->insert([
            'name' => $name,
            'sort' => $sort,
            'create_time' => $now,
            'update_time' => $now,
        ]);

        return ['code' => 0, 'msg' => '', 'data' => ['id' => $newId]];
    }

    /**
     * 删除媒体文件分组
     * @param int $id
     * @return array
     */
    public function deleteGroup(int $id): array
    {
        if ($id <= 0) {
            return ['code' => 1, 'msg' => ''];
        }

        if (!$this->groupRepository()->findById($id)) {
            return ['code' => 1, 'msg' => ''];
        }

        try {
            $this->fileRepository()->clearGroupId($id);
        } catch (\Throwable $e) {
        }

        $this->groupRepository()->deleteById($id);
        return ['code' => 0, 'msg' => ''];
    }

    /**
     * 归一化媒体文件分组ID
     * @param int $groupId
     * @return int
     */
    public function normalizeGroupId(int $groupId): int
    {
        if ($groupId <= 0) {
            return 0;
        }

        return $this->groupRepository()->findById($groupId) ? $groupId : 0;
    }

    /**
     * 删除本地物理文件
     * @param string $path 文件相对路径
     * @return void
     */
    private function deletePhysicalFile(string $path): void
    {
        if ($path === '') {
            return;
        }

        try {
            Filesystem::disk('public')->delete($path);
        } catch (\Throwable $e) {
        }
    }

    /**
     * 删除缩略图文件
     * @param string $thumbnailsJson 缩略图JSON字符串
     * @return void
     */
    private function deleteThumbnailFiles(string $thumbnailsJson): void
    {
        if ($thumbnailsJson === '') {
            return;
        }
        $thumbnails = json_decode($thumbnailsJson, true);
        if (!is_array($thumbnails)) {
            return;
        }
        foreach ($thumbnails as $thumbPath) {
            if (is_string($thumbPath) && $thumbPath !== '') {
                try {
                    Filesystem::disk('public')->delete($thumbPath);
                } catch (\Throwable $e) {
                }
            }
        }
    }

    /**
     * 获取指定类型允许的文件扩展名列表
     * @param string $type 文件类型（image/video/file）
     * @return array
     */
    private function allowedExts(string $type): array
    {
        if ($type === 'image') {
            return ['jpg', 'jpeg', 'png', 'webp', 'gif'];
        }
        if ($type === 'video') {
            return ['mp4', 'webm', 'mov'];
        }
        return ['pdf', 'zip', 'rar', '7z', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'apk', 'wgt', 'mp3', 'wav', 'aac', 'm4a'];
    }

    /**
     * 获取指定类型的文件大小上限（字节）
     * @param string $type 文件类型（image/video/file）
     * @return int
     */
    private function maxSizeBytes(string $type): int
    {
        if ($type === 'image') {
            return 10 * 1024 * 1024;
        }
        if ($type === 'video') {
            return 200 * 1024 * 1024;
        }
        return 500 * 1024 * 1024;
    }

    /**
     * 根据文件扩展名推断媒体类型
     * @param string $ext 文件扩展名
     * @return string 媒体类型（image/video/file）
     */
    private function inferTypeByExt(string $ext): string
    {
        $ext = strtolower(trim($ext));
        if ($ext === '') {
            return 'file';
        }
        if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true)) {
            return 'image';
        }
        if (in_array($ext, ['mp4', 'webm', 'mov'], true)) {
            return 'video';
        }
        return 'file';
    }

    /**
     * 归一化原始文件名
     * @param string $name
     * @return string
     */
    private function normalizeOriginalName(string $name): string
    {
        $clean = trim(str_replace(["\0", "\r", "\n"], '', str_replace('\\', '/', $name)));
        $clean = basename($clean);
        if ($clean === '') {
            return '';
        }
        return $this->truncateText($clean, 255);
    }

    /**
     * 验证文件内容MIME类型（通过魔数字节）
     * @param string $realPath
     * @param string $type
     * @return bool
     */
    private function validateMime(string $realPath, string $type): bool
    {
        if (!extension_loaded('fileinfo')) {
            return true; // best-effort: skip if finfo unavailable
        }
        try {
            $finfo = new \finfo(FILEINFO_MIME_TYPE);
            $detected = $finfo->file($realPath);
            if ($detected === false) {
                return false;
            }
        } catch (\Throwable $e) {
            return false;
        }

        $allowedMimes = $this->allowedMimes($type);
        return in_array($detected, $allowedMimes, true);
    }

    /**
     * 获取允许的MIME类型
     * @param string $type
     * @return array
     */
    private function allowedMimes(string $type): array
    {
        if ($type === 'image') {
            return ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
        }
        if ($type === 'video') {
            return ['video/mp4', 'video/webm', 'video/quicktime'];
        }
        // file: allow common document/archive types
        return [
            'application/pdf',
            'application/zip',
            'application/x-rar-compressed', 'application/vnd.rar',
            'application/x-7z-compressed',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-powerpoint',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'text/plain',
            'application/vnd.android.package-archive',
            'application/octet-stream', // wgt / generic binary
            'audio/mpeg', 'audio/wav', 'audio/aac', 'audio/x-m4a',
        ];
    }

    /**
     * 截断文本
     * @param string $value
     * @param int $length
     * @return string
     */
    private function truncateText(string $value, int $length): string
    {
        if ($length <= 0 || $value === '') {
            return '';
        }
        if (function_exists('mb_substr')) {
            return mb_substr($value, 0, $length);
        }
        return substr($value, 0, $length);
    }

    /**
     * 获取文件仓库
     * @return MediaFileRepository
     */
    private function fileRepository(): MediaFileRepository
    {
        return $this->app->make(MediaFileRepository::class);
    }

    /**
     * 获取文件组仓库
     * @return MediaGroupRepository
     */
    private function groupRepository(): MediaGroupRepository
    {
        return $this->app->make(MediaGroupRepository::class);
    }
}