<?php

namespace XZD\AI\V5\Tools;

use XZD\AI\V5\Core\PreserveDates;

class MediaManager
{
    // ═══════════════════════════════════════════════
    // 图片发现 — 流水线状态感知
    // ═══════════════════════════════════════════════

    /**
     * 查找文章中的图片 — 优先读取流水线状态，回退到内容扫描
     *
     * @return array 每一项 ['url' => string, 'path' => string|null, 'tag' => string|null]
     */
    private static function findPostImages(int $postId): array
    {
        $images = [];

        // 优先：读取 _xzd_localized_images meta（流水线状态传递）
        $localizedMap = get_post_meta($postId, '_xzd_localized_images', true);
        if (!empty($localizedMap) && is_array($localizedMap)) {
            // 读取文章内容，用于查找 <img> 标签（供 WebP 等需要替换内容的阶段使用）
            $post = get_post($postId);
            $postContent = $post ? $post->post_content : '';
            foreach ($localizedMap as $oldUrl => $localUrl) {
                $localPath = self::urlToPath($localUrl);
                if ($localPath && file_exists($localPath)) {
                    // 同时在内容中查找原始URL和本地URL的<img>标签
                    $tag = null;
                    // 先查找本地URL
                    $escapedLocal = preg_quote($localUrl, '/');
                    if (preg_match('/<img[^>]*\ssrc=["\']' . $escapedLocal . '(?:\?[^"\']*)?["\'][^>]*>/i', $postContent, $tagMatch)) {
                        $tag = $tagMatch[0];
                    }
                    // 再查找原始URL（内容可能还未更新）
                    if (!$tag && $oldUrl !== $localUrl) {
                        $escapedOld = preg_quote($oldUrl, '/');
                        if (preg_match('/<img[^>]*\ssrc=["\']' . $escapedOld . '(?:\?[^"\']*)?["\'][^>]*>/i', $postContent, $tagMatch)) {
                            $tag = $tagMatch[0];
                        }
                    }
                    $images[] = ['url' => $localUrl, 'path' => $localPath, 'tag' => $tag];
                }
            }
            if (!empty($images)) return $images;
        }

        // 回退：扫描文章内容 <img> 标签
        $post = get_post($postId);
        if (!$post) return [];

        preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches);
        $srcs = $matches[1] ?? [];
        $tags = $matches[0] ?? [];

        foreach ($srcs as $i => $src) {
            $cleanSrc = preg_replace('/[?#].*$/', '', $src);
            $localPath = self::urlToPath($cleanSrc);
            if ($localPath && file_exists($localPath)) {
                $images[] = ['url' => $src, 'path' => $localPath, 'tag' => $tags[$i] ?? null];
            }
        }

        return $images;
    }

    /**
     * URL → 本地绝对路径
     */
    private static function urlToPath(string $url): ?string
    {
        $uploadDir = wp_upload_dir();
        $baseUrl = $uploadDir['baseurl'];
        if (str_starts_with($url, $baseUrl)) {
            return $uploadDir['basedir'] . substr($url, strlen($baseUrl));
        }
        // 也尝试匹配 site_url 路径
        $siteUrl = site_url();
        if (str_starts_with($url, $siteUrl)) {
            $parsed = parse_url($siteUrl);
            $basePath = $parsed['path'] ?? '';
            $relPath = substr($url, strlen($siteUrl));
            return rtrim(ABSPATH, '/') . '/' . ltrim($basePath . $relPath, '/');
        }
        return null;
    }

    /**
     * 确保 xzd-localized 目录存在并可写
     */
    private static function ensureLocalizedDir(): string
    {
        $uploadDir = wp_upload_dir();
        $xzdDir = $uploadDir['basedir'] . '/xzd-localized';
        if (!file_exists($xzdDir)) {
            wp_mkdir_p($xzdDir);
        }
        if (!file_exists($xzdDir . '/index.php')) {
            file_put_contents($xzdDir . '/index.php', '<?php // silence');
        }
        return $xzdDir;
    }

    /**
     * 替换图片标签中的URL（区分属性类型）
     *
     * - src: 直接替换 src 属性中的 URL
     * - data-src 等懒加载属性: 替换懒加载属性 + 补齐 src 属性
     */
    private static function replaceImageUrl(string $tag, string $oldUrl, string $newUrl, string $attr): string
    {
        if ($attr === 'src') {
            return str_replace($oldUrl, $newUrl, $tag);
        }
        // 懒加载属性：替换属性值 + 设置 src
        $newTag = str_replace($oldUrl, $newUrl, $tag);
        // 如果标签没有 src 属性或 src 是小占位图，补齐 src
        if (!preg_match('/\ssrc=["\'][^"\']+["\']/', $newTag)) {
            $newTag = str_replace('<img', '<img src="' . esc_url($newUrl) . '"', $newTag);
        }
        return $newTag;
    }

    /**
     * 获取文件扩展名（参考 XZW 的 kaninai_plugin_getType）
     */
    private static function getImageType(string $path): string
    {
        $info = getimagesize($path);
        if (!$info) return 'jpg';
        return match ($info[2]) {
            1       => 'gif',
            2       => 'jpeg',
            3       => 'png',
            15      => 'wbmp',
            18      => 'webp',
            default => 'png',
        };
    }

    // ═══════════════════════════════════════════════
    // 图片本地化
    // ═══════════════════════════════════════════════

    /**
     * 图片本地化 — 下载外部图片到本地并替换文章中的URL
     *
     * 参考 XZW Knn_local::localImage() 的 MD5+glob 去重方案，
     * 使用 wp_remote_get() 直接下载，存入 /uploads/xzd-localized/
     */
    public static function localizeImage(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error('文章不存在');

        $maxImages = (int) ($params['max_images'] ?? 20);
        $xzdDir = self::ensureLocalizedDir();
        $uploadDir = wp_upload_dir();

        // 收集所有图片URL及其标签和属性（支持 src + 懒加载属性）
        $imageEntries = []; // 每项: ['url' => '', 'tag' => '', 'attr' => 'src|data-src|...']

        // 1. 标准 src 属性
        preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $m);
        foreach ($m[1] as $idx => $url) {
            $imageEntries[] = ['url' => $url, 'tag' => $m[0][$idx], 'attr' => 'src'];
        }

        // 2. 懒加载属性（Zibll主题、WP延迟加载、a3 Lazy Load等常见插件）
        $lazyAttrs = ['data-src', 'data-lazy-src', 'data-original', 'data-lazy-srcset'];
        foreach ($lazyAttrs as $attr) {
            preg_match_all('/<img[^>]+' . $attr . '=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $m);
            foreach ($m[1] as $idx => $url) {
                // 跳过 data-lazy-srcset（包含多个URL）
                if ($attr === 'data-lazy-srcset') continue;
                // 避免重复（同一标签可能已通过 src 收集）
                $tag = $m[0][$idx];
                $alreadyCollected = false;
                foreach ($imageEntries as $entry) {
                    if ($entry['tag'] === $tag) { $alreadyCollected = true; break; }
                }
                if (!$alreadyCollected) {
                    $imageEntries[] = ['url' => $url, 'tag' => $tag, 'attr' => $attr];
                }
            }
        }

        if (empty($imageEntries)) {
            return ToolResult::success(['localized' => 0, 'total' => 0], '文章中没有图片');
        }

        $homeUrl = home_url();
        $content = $post->post_content;
        $localized = 0;
        $skipped = 0;
        $failed = 0;
        $errors = [];
        $processed = 0;
        $localizedMap = get_post_meta($postId, '_xzd_localized_images', true) ?: [];

        foreach ($imageEntries as $entry) {
            if ($processed >= $maxImages) break;

            $url = $entry['url'];
            $oldTag = $entry['tag'];
            $attr = $entry['attr'];

            // 跳过已是本站的URL
            if (str_contains($url, $homeUrl)) {
                $skipped++;
                continue;
            }

            // 修复协议相对URL
            $downloadUrl = str_starts_with($url, '//') ? 'https:' . $url : $url;

            // 跳过无效URL
            if (!str_starts_with($downloadUrl, 'http')) {
                $skipped++;
                continue;
            }

            $processed++;

            // MD5去重检查
            $urlMd5 = md5($downloadUrl);
            $existing = glob($xzdDir . '/' . $urlMd5 . '.*');
            if (!empty($existing)) {
                $localUrl = $uploadDir['baseurl'] . '/xzd-localized/' . basename($existing[0]);
                $newTag = self::replaceImageUrl($oldTag, $url, $localUrl, $attr);
                if ($newTag !== $oldTag) {
                    $content = str_replace($oldTag, $newTag, $content);
                    $localized++;
                    $localizedMap[$url] = $localUrl;
                }
                continue;
            }

            // 下载图片（模拟浏览器请求）
            $parsedUrl = parse_url($downloadUrl);
            $headers = [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
                'Referer'    => ($parsedUrl['scheme'] ?? 'https') . '://' . ($parsedUrl['host'] ?? ''),
            ];
            $response = wp_remote_get($downloadUrl, [
                'headers'   => $headers,
                'sslverify' => false,
                'timeout'   => 30,
            ]);

            if (is_wp_error($response)) {
                $failed++;
                $errors[] = "下载失败: {$downloadUrl} — " . $response->get_error_message();
                continue;
            }

            $httpCode = wp_remote_retrieve_response_code($response);
            if ($httpCode !== 200) {
                $failed++;
                $errors[] = "下载失败: {$downloadUrl} — HTTP {$httpCode}";
                continue;
            }

            $body = wp_remote_retrieve_body($response);
            if (empty($body)) {
                $failed++;
                $errors[] = "下载失败: {$downloadUrl} — 空内容";
                continue;
            }

            // 写入临时文件，检测类型后重命名
            $tmpFile = $xzdDir . '/' . $urlMd5;
            if (!file_put_contents($tmpFile, $body, LOCK_EX)) {
                $failed++;
                $errors[] = "写入失败: {$downloadUrl}";
                continue;
            }

            $ext = self::getImageType($tmpFile);
            if ($ext === 'jpeg') $ext = 'jpg';
            $finalFile = $tmpFile . '.' . $ext;
            rename($tmpFile, $finalFile);

            $localUrl = $uploadDir['baseurl'] . '/xzd-localized/' . basename($finalFile);

            // 替换文章中的外部URL（区分属性类型）
            $newTag = self::replaceImageUrl($oldTag, $url, $localUrl, $attr);
            if ($newTag !== $oldTag) {
                $content = str_replace($oldTag, $newTag, $content);
                $localized++;
                $localizedMap[$url] = $localUrl;
            }
        }

        // 保存文章内容 + 始终写入meta（即使无外部图片，供下游WebP/水印阶段使用）
        if ($localized > 0) {
            PreserveDates::updatePost([
                'ID'           => $postId,
                'post_content' => $content,
            ]);
        }

        // 始终写入流水线状态，扫描所有图片（本地+刚下载的）
        $allImageMeta = $localizedMap;
        // 补充已有的本地图片路径（src + 懒加载属性）
        $allSrcUrls = [];
        preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/', $post->post_content, $m);
        $allSrcUrls = array_merge($allSrcUrls, $m[1] ?? []);
        foreach (['data-src', 'data-lazy-src', 'data-original'] as $lazyAttr) {
            preg_match_all('/<img[^>]+' . $lazyAttr . '=["\']([^"\']+)["\']/', $post->post_content, $m);
            $allSrcUrls = array_merge($allSrcUrls, $m[1] ?? []);
        }
        foreach (array_unique($allSrcUrls) as $imgUrl) {
            $cleanUrl = preg_replace('/[?#].*$/', '', $imgUrl);
            $localPath = self::urlToPath($cleanUrl);
            if ($localPath && file_exists($localPath) && !isset($allImageMeta[$imgUrl])) {
                $allImageMeta[$imgUrl] = $imgUrl; // 本地图片保留原URL
            }
        }
        update_post_meta($postId, '_xzd_localized_images', $allImageMeta);
        update_post_meta($postId, '_xzd_ai_processed_localize_image', current_time('mysql'));

        return ToolResult::success([
            'localized'     => $localized,
            'skipped'       => $skipped,
            'failed'        => $failed,
            'total'         => count($imageEntries),
            'errors'        => $errors,
            'localized_map' => $localizedMap,
        ], "已本地化 {$localized}/{$processed} 张图片，跳过 {$skipped} 张，失败 {$failed} 张");
    }

    // ═══════════════════════════════════════════════
    // WebP 转换
    // ═══════════════════════════════════════════════

    /**
     * WebP转换 — 将文章内PNG/JPEG转为WebP，使用 <picture> 标签保留原图降级
     *
     * 通过 findPostImages() 直接获取本地文件路径，
     * 优先 GD 直接操作（参考 XZW），回退到 wp_get_image_editor
     */
    public static function convertToWebp(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        if (!$postId) return ToolResult::error('请提供文章ID');

        $post = get_post($postId);
        if (!$post) return ToolResult::error('文章不存在');

        $quality   = (int) ($params['quality'] ?? 85);
        $maxImages = (int) ($params['max_images'] ?? 20);

        if (!function_exists('imagewebp')) {
            return ToolResult::error('服务器环境不支持WebP格式（需要PHP GD with WebP支持）');
        }

        $images = self::findPostImages($postId);
        if (empty($images)) {
            return ToolResult::success(['converted' => 0, 'total' => 0], '文章中没有可转换的本地图片');
        }

        $converted = 0;
        $skipped = 0;
        $content = $post->post_content;
        $processed = 0;

        foreach ($images as $img) {
            if ($processed >= $maxImages) break;
            $localPath = $img['path'];
            if (!$localPath || !file_exists($localPath)) { $skipped++; continue; }

            // 跳过已是webp
            $ext = strtolower(pathinfo($localPath, PATHINFO_EXTENSION));
            if ($ext === 'webp') { $skipped++; continue; }

            // 只处理 JPG/PNG
            if (!in_array($ext, ['jpg', 'jpeg', 'png'])) { $skipped++; continue; }

            $webpPath = preg_replace('/\.(jpe?g|png)$/i', '.webp', $localPath);

            // WebP已存在且比原图小 → 跳过转换但修复内容引用
            if (file_exists($webpPath)) {
                $webpSize = filesize($webpPath);
                $origSize = filesize($localPath);
                if ($webpSize > $origSize) {
                    @unlink($webpPath);
                    // 删除过大webp后继续转换
                } else {
                    // WebP已存在且有效，直接修复内容引用
                    $processed++;
                    $skipped++;
                    $localUrl = $img['url'];
                    $webpUrl = str_replace('.' . $ext, '.webp', $localUrl);
                    if (self::replaceImgSrcInContent($content, $localUrl, $webpUrl)) {
                        $converted++;
                    }
                    continue;
                }
            }

            $processed++;
            $origSize = filesize($localPath);

            // 优先GD直接转（参考XZW towebp方法），回退到wp_get_image_editor
            $saved = self::gdConvertToWebp($localPath, $webpPath, $ext, $quality);

            if (!$saved) {
                // 回退到 WordPress 图片编辑器
                $imgEditor = wp_get_image_editor($localPath);
                if (is_wp_error($imgEditor)) { $skipped++; continue; }
                $imgEditor->set_quality($quality);
                $saved = $imgEditor->save($webpPath, 'image/webp');
                if (is_wp_error($saved)) { $skipped++; continue; }
                $webpPath = $saved['path'];
            }

            // WebP比原图还大则跳过
            $webpSize = filesize($webpPath);
            if ($webpSize > $origSize) {
                @unlink($webpPath);
                $skipped++;
                continue;
            }

            // 替换文章内容：img src → webp
            $localUrl = $img['url'];
            $webpUrl = str_replace('.' . $ext, '.webp', $localUrl);

            if (self::replaceImgSrcInContent($content, $localUrl, $webpUrl, $img['tag'] ?? null)) {
                $converted++;
            }
        }

        if ($converted > 0) {
            PreserveDates::updatePost(['ID' => $postId, 'post_content' => $content]);
        }

        update_post_meta($postId, '_xzd_ai_webp_converted', current_time('mysql'));
        update_post_meta($postId, '_xzd_ai_webp_count', $converted);

        return ToolResult::success([
            'converted' => $converted,
            'skipped'   => $skipped,
            'total'     => count($images),
        ], "已将 {$converted}/" . count($images) . " 张图片转为 WebP（跳过 {$skipped} 张）");
    }

    /**
     * GD直接转WebP（参考 XZW towebp 方法）
     */
    private static function gdConvertToWebp(string $source, string $dest, string $ext, int $quality): bool
    {
        try {
            $ctx = match ($ext) {
                'jpg', 'jpeg' => @imagecreatefromjpeg($source),
                'png'         => function () use ($source) {
                    $img = @imagecreatefrompng($source);
                    if ($img) {
                        imagepalettetotruecolor($img);
                        imagealphablending($img, true);
                        imagesavealpha($img, true);
                    }
                    return $img;
                },
                'gif'         => @imagecreatefromgif($source),
                'wbmp'        => @imagecreatefromwbmp($source),
                default       => null,
            };

            // Handle closure for PNG
            if ($ctx instanceof \Closure) {
                $ctx = $ctx();
            }

            if (!$ctx) return false;

            $result = imagewebp($ctx, $dest, $quality);
            imagedestroy($ctx);
            return $result;
        } catch (\Throwable $e) {
            return false;
        }
    }

    // ═══════════════════════════════════════════════
    // 水印
    // ═══════════════════════════════════════════════

    /**
     * 应用水印 — GD/Imagick 真实图片合成
     *
     * 通过 findPostImages() 直接获取本地文件路径，
     * 不再依赖 wp-image-{id} CSS类
     */
    public static function applyWatermark(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        if (!$postId) return ToolResult::error('请提供文章ID');

        $post = get_post($postId);
        if (!$post) return ToolResult::error('文章不存在');

        // 解析水印图来源
        $watermarkId = (int) ($params['watermark_id'] ?? 0);
        if (!$watermarkId) {
            $watermarkId = (int) (\XZD\AI\V5\Core\Config::instance()->get('watermark.default_image_id', 0));
        }
        if (!$watermarkId) {
            return ToolResult::success(['watermarked' => 0, 'total' => 0], '未配置水印图片，跳过');
        }

        $watermarkPath = get_attached_file($watermarkId);
        if (!$watermarkPath || !file_exists($watermarkPath)) {
            return ToolResult::error('水印图片文件不存在，请重新上传');
        }

        $position = $params['position'] ?? 'bottom-right';
        $opacity  = (int) ($params['opacity'] ?? 50);
        $sizePct  = (int) ($params['size_pct'] ?? 15);

        $wmInfo = getimagesize($watermarkPath);
        if (!$wmInfo) return ToolResult::error('无法读取水印图片');

        $images = self::findPostImages($postId);
        if (empty($images)) {
            return ToolResult::success(['watermarked' => 0, 'total' => 0], '文章中没有可加水印的本地图片');
        }

        $hasImagick = class_exists('Imagick') && extension_loaded('imagick');
        $watermarked = 0;
        $skipped = 0;

        foreach ($images as $img) {
            $path = $img['path'];
            if (!$path || !file_exists($path)) { $skipped++; continue; }

            $ok = $hasImagick
                ? self::applyWatermarkImagick($path, $watermarkPath, $position, $opacity, $sizePct)
                : false;

            // Imagick失败时回退到GD
            if (!$ok) {
                $ok = self::applyWatermarkGD($path, $watermarkPath, $position, $opacity, $sizePct);
            }

            if ($ok) {
                $watermarked++;
            } else {
                $skipped++;
            }
        }

        update_post_meta($postId, '_xzd_ai_watermarked', current_time('mysql'));
        return ToolResult::success(
            ['watermarked' => $watermarked, 'skipped' => $skipped, 'total' => count($images)],
            "已为 {$watermarked}/" . count($images) . " 张图片添加水印"
        );
    }

    /** Imagick 水印合成 */
    private static function applyWatermarkImagick(string $imagePath, string $wmPath, string $position, int $opacity, int $sizePct): bool
    {
        try {
            $img = new \Imagick($imagePath);
            $wm  = new \Imagick($wmPath);

            $imgW = $img->getImageWidth();
            $imgH = $img->getImageHeight();

            $targetW = (int) ($imgW * $sizePct / 100);
            if ($targetW < 50) $targetW = 50;
            $wm->scaleImage($targetW, 0);
            $wmW = $wm->getImageWidth();
            $wmH = $wm->getImageHeight();

            $padding = 20;
            [$x, $y] = self::calcPosition($imgW, $imgH, $wmW, $wmH, $position, $padding);

            if ($opacity < 100) {
                $wm->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity / 100, \Imagick::CHANNEL_ALPHA);
            }

            $img->compositeImage($wm, \Imagick::COMPOSITE_OVER, $x, $y);
            $img->writeImage($imagePath);
            $img->destroy();
            $wm->destroy();
            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /** GD 水印合成（降级方案） */
    private static function applyWatermarkGD(string $imagePath, string $wmPath, string $position, int $opacity, int $sizePct): bool
    {
        $imgInfo = getimagesize($imagePath);
        if (!$imgInfo) return false;

        $mime = $imgInfo['mime'];
        switch ($mime) {
            case 'image/jpeg': $img = imagecreatefromjpeg($imagePath); break;
            case 'image/png':  $img = imagecreatefrompng($imagePath); break;
            case 'image/webp': $img = imagecreatefromwebp($imagePath); break;
            default: return false;
        }
        if (!$img) return false;

        // 根据水印文件类型加载（支持PNG/JPEG/WEBP）
        $wmInfo = getimagesize($wmPath);
        $wmMime = $wmInfo['mime'] ?? '';
        $wm = match ($wmMime) {
            'image/png'  => imagecreatefrompng($wmPath),
            'image/jpeg' => imagecreatefromjpeg($wmPath),
            'image/webp' => imagecreatefromwebp($wmPath),
            default      => imagecreatefrompng($wmPath), // 回退尝试PNG
        };
        if (!$wm) {
            // 回退：尝试所有格式
            $wm = @imagecreatefromwebp($wmPath)
                ?: @imagecreatefrompng($wmPath)
                ?: @imagecreatefromjpeg($wmPath);
        }
        if (!$wm) { imagedestroy($img); return false; }

        $imgW = imagesx($img);
        $imgH = imagesy($img);
        $wmW  = imagesx($wm);
        $wmH  = imagesy($wm);

        $targetW = (int) ($imgW * $sizePct / 100);
        if ($targetW < 50) $targetW = 50;
        $ratio = $targetW / $wmW;
        $targetH = (int) ($wmH * $ratio);
        $targetW = (int) ($wmW * $ratio);

        $wmResized = imagecreatetruecolor($targetW, $targetH);
        imagealphablending($wmResized, false);
        imagesavealpha($wmResized, true);
        imagecopyresampled($wmResized, $wm, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH);
        imagedestroy($wm);
        $wm = $wmResized;
        $wmW = $targetW;
        $wmH = $targetH;

        $padding = 20;
        [$x, $y] = self::calcPosition($imgW, $imgH, $wmW, $wmH, $position, $padding);

        imagealphablending($img, true);
        imagesavealpha($img, true);
        imagecopymerge($img, $wm, $x, $y, 0, 0, $wmW, $wmH, $opacity);

        switch ($mime) {
            case 'image/jpeg': imagejpeg($img, $imagePath, 90); break;
            case 'image/png':  imagepng($img, $imagePath, 9); break;
            case 'image/webp': imagewebp($img, $imagePath, 85); break;
        }

        imagedestroy($img);
        imagedestroy($wm);
        return true;
    }

    /**
     * 替换文章内容中的图片 src URL（用于WebP/压缩等）
     *
     * @param string $content  文章内容（引用传递）
     * @param string $oldUrl   原始图片URL
     * @param string $newUrl   新图片URL
     * @param string|null $tag 已知的img标签（可选，优先使用）
     * @return bool 是否成功替换
     */
    private static function replaceImgSrcInContent(string &$content, string $oldUrl, string $newUrl, ?string $tag = null): bool
    {
        // 方式1：有已知标签，直接替换该标签中的src
        if ($tag) {
            $newTag = preg_replace(
                '/src=["\']' . preg_quote($oldUrl, '/') . '(?:\?[^"\']*)?["\']/i',
                'src="' . esc_url($newUrl) . '"',
                $tag
            );
            if ($newTag !== $tag) {
                $content = str_replace($tag, $newTag, $content);
                return true;
            }
        }

        // 方式2：在内容中按URL模糊匹配替换
        $pattern = '/(<img[^>]*\ssrc=["\'])' . preg_quote($oldUrl, '/') . '(\?[^"\']*)?(["\'])/i';
        $newContent = preg_replace($pattern, '$1' . esc_url($newUrl) . '$3', $content, 1, $count);
        if ($count > 0) {
            $content = $newContent;
            return true;
        }

        return false;
    }

    /** 计算9宫格水印坐标 */
    private static function calcPosition(int $imgW, int $imgH, int $wmW, int $wmH, string $position, int $padding): array
    {
        $map = [
            'top-left'      => [$padding, $padding],
            'top-center'    => [($imgW - $wmW) / 2, $padding],
            'top-right'     => [$imgW - $wmW - $padding, $padding],
            'center-left'   => [$padding, ($imgH - $wmH) / 2],
            'center'        => [($imgW - $wmW) / 2, ($imgH - $wmH) / 2],
            'center-right'  => [$imgW - $wmW - $padding, ($imgH - $wmH) / 2],
            'bottom-left'   => [$padding, $imgH - $wmH - $padding],
            'bottom-center' => [($imgW - $wmW) / 2, $imgH - $wmH - $padding],
            'bottom-right'  => [$imgW - $wmW - $padding, $imgH - $wmH - $padding],
        ];
        return $map[$position] ?? $map['bottom-right'];
    }

    // ═══════════════════════════════════════════════
    // 图片压缩
    // ═══════════════════════════════════════════════

    /**
     * 压缩图片 — 通过 findPostImages() 获取本地文件直接压缩
     */
    public static function compressImage(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        if (!$postId) return ToolResult::error('请提供文章ID');

        $images = self::findPostImages($postId);
        if (empty($images)) {
            return ToolResult::success(['compressed' => 0, 'saved_kb' => 0], '文章中没有可压缩的本地图片');
        }

        $quality = (int) ($params['quality'] ?? 85);
        $compressed = 0;
        $totalBefore = 0;
        $totalAfter = 0;

        foreach ($images as $img) {
            $path = $img['path'];
            if (!$path || !file_exists($path)) continue;

            $before = filesize($path);
            $editor = wp_get_image_editor($path);
            if (is_wp_error($editor)) continue;

            $editor->set_quality($quality);
            $editor->save($path);

            $after = filesize($path);
            $totalBefore += $before;
            $totalAfter += $after;
            $compressed++;
        }

        $savedKb = $totalBefore > 0 ? round(($totalBefore - $totalAfter) / 1024, 1) : 0;
        return ToolResult::success(
            ['compressed' => $compressed, 'saved_kb' => $savedKb],
            "已压缩 {$compressed} 张图片，节省 {$savedKb}KB"
        );
    }

    // ═══════════════════════════════════════════════
    // 一键处理 — 组合工具
    // ═══════════════════════════════════════════════

    /**
     * 一键处理文章图片：本地化 → WebP → 水印 → 压缩
     *
     * 供 AI 对话直接调用，也作为流水线阶段。
     * 各步骤共享状态（通过 _xzd_localized_images post meta）。
     */
    public static function processImages(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        if (!$postId) return ToolResult::error('请提供文章ID');

        $results = [];
        $allSuccess = true;
        $localizedCount = 0;
        $webpCount = 0;
        $watermarkCount = 0;
        $compressCount = 0;
        $skipLocalize = !empty($params['skip_localize']);

        // 1. 本地化
        if (!$skipLocalize) {
            $r1 = self::localizeImage($params, $userId);
            $results['localize'] = [
                'success' => $r1->success,
                'message' => $r1->message,
                'data'    => $r1->data,
            ];
            $localizedCount = $r1->data['localized'] ?? 0;
            if (!$r1->success) $allSuccess = false;

            // 如果本地化后没有图片，跳过后续步骤
            if ($localizedCount === 0 && ($r1->data['skipped'] ?? 0) === ($r1->data['total'] ?? 0)) {
                $results['webp'] = ['skipped' => true, 'reason' => '无可本地化图片'];
                $results['watermark'] = ['skipped' => true, 'reason' => '无本地图片'];
                $results['compress'] = ['skipped' => true, 'reason' => '无本地图片'];
                return ToolResult::success($results, '图片处理完成（无外部图片需要本地化）');
            }
        }

        // 2. WebP转换
        $r2 = self::convertToWebp(array_merge($params, ['post_id' => $postId]), $userId);
        $results['webp'] = [
            'success' => $r2->success,
            'message' => $r2->message,
            'data'    => $r2->data,
        ];
        $webpCount = $r2->data['converted'] ?? 0;

        // 3. 水印（即使没有配置水印也不报错）
        $r3 = self::applyWatermark($params, $userId);
        $results['watermark'] = [
            'success' => $r3->success,
            'message' => $r3->message,
            'data'    => $r3->data,
        ];
        $watermarkCount = $r3->data['watermarked'] ?? 0;

        // 4. 压缩
        $r4 = self::compressImage($params, $userId);
        $results['compress'] = [
            'success' => $r4->success,
            'message' => $r4->message,
            'data'    => $r4->data,
        ];
        $compressCount = $r4->data['compressed'] ?? 0;
        $savedKb = $r4->data['saved_kb'] ?? 0;

        $summary = sprintf(
            '图片处理完成: 本地化 %d 张, WebP %d 张, 水印 %d 张, 压缩 %d 张%s',
            $localizedCount, $webpCount, $watermarkCount, $compressCount,
            $savedKb > 0 ? " (节省 {$savedKb}KB)" : ''
        );

        return ToolResult::success($results, $summary);
    }

    // ═══════════════════════════════════════════════
    // 图库搜索 / 内联插图
    // ═══════════════════════════════════════════════

    /**
     * 搜索图库（免费图库 + 公共域保底）
     */
    public static function searchStock(array $params, int $userId): ToolResult
    {
        $query   = $params['query'] ?? '';
        $perPage = (int)($params['per_page'] ?? 10);

        if (empty($query)) {
            return ToolResult::error('请提供搜索关键词');
        }

        $imageClient = \XZD\AI\V5\AI\ImageClient::instance();

        $result = $imageClient->searchStock($query, $perPage);
        if ($result['success'] && !empty($result['images'])) {
            return ToolResult::success([
                'images' => $result['images'],
                'total'  => $result['total'] ?? count($result['images']),
                'source' => 'stock',
            ], "图库搜索: {$query}");
        }

        $pubResult = $imageClient->searchPublicDomain($query, 1024, 768);
        return ToolResult::success([
            'images' => $pubResult['images'] ?? [],
            'total'  => $pubResult['total'] ?? 0,
            'source' => 'public',
        ], "公共域图库: {$query} (无需API配置)");
    }

    /**
     * 为文章生成内联插图
     */
    public static function generateInlineImage(array $params, int $userId): ToolResult
    {
        $postId      = (int)($params['post_id'] ?? 0);
        $contextText = $params['context_text'] ?? '';
        $source      = $params['source'] ?? null;

        if (!$postId) {
            return ToolResult::error('请提供文章ID');
        }

        if (empty($contextText)) {
            $post = get_post($postId);
            $contextText = $post ? $post->post_title : '';
        }

        $imgClient = \XZD\AI\V5\AI\ImageClient::instance();
        $hasAI = $imgClient->hasImageProvider();

        if (!$source && $hasAI) {
            $sources = $imgClient->getAvailableSources();
            return ToolResult::success([
                'post_id'          => $postId,
                'context_text'     => $contextText,
                'source_selection' => true,
                'choices'          => $sources,
                'message'          => '检测到已配置 AI 生图模型，请选择插图来源：',
            ], '请选择插图来源');
        }

        $effectiveSource = $source ?: 'stock';

        $result = $imgClient->generateInlineImage(
            $postId, $contextText, ['preferred_source' => $effectiveSource]
        );

        if (!$result['success']) {
            return ToolResult::error($result['message'] ?? '插图生成失败');
        }

        $sourceLabel = ['ai' => 'AI 生图', 'stock' => '免费图库', 'public' => '公共域图库'][$effectiveSource] ?? $effectiveSource;
        $note = $hasAI ? '' : '（未配置 AI 生图模型，使用' . $sourceLabel . '）';

        return ToolResult::success([
            'attachment_id' => $result['attachment_id'],
            'url'           => $result['url'],
            'source'        => $result['source'],
        ], "插图已生成 (来源: {$sourceLabel}){$note}");
    }
}
