<?php
// +----------------------------------------------------------------------
// | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2023 https://www.yiovo.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
// +----------------------------------------------------------------------
// | Author: 萤火科技 <admin@yiovo.com>
// +----------------------------------------------------------------------
declare (strict_types=1);

namespace app\job\service\goods;

use app\store\service\Upload as UploadService;
use app\common\service\BaseService;
use app\common\model\Spec as SpecModel;
use app\common\model\Goods as GoodsModel;
use app\common\model\GoodsSku as GoodsSkuModel;
use app\common\model\GoodsImage as GoodsImageModel;
use app\common\model\store\Setting as SettingModel;
use app\common\model\GoodsSpecRel as GoodsSpecRelModel;
use app\common\model\goods\Collector as GoodsCollectorModel;
use app\common\model\GoodsCategoryRel as GoodsCategoryRelModel;
use app\common\enum\Setting as SettingEnum;
use app\common\enum\file\FileType as FileTypeEnum;
use app\common\enum\goods\SpecType as GoodsSpecTypeEnum;
use app\common\enum\goods\DeductStockType as DeductStockTypeEnum;
use app\common\enum\goods\CollectorStatus as GoodsCollectorStatusEnum;
use app\common\library\helper;
use app\common\library\Download;
use app\common\library\collector\tools\UrlStore;
use app\common\library\collector\tools\UrlItemId;
use app\common\library\collector\Facade as CollectorFacade;
use cores\exception\BaseException;

/**
 * 服务类:商品批量采集
 * Class Collector
 * @package app\job\service\goods
 */
class Collector extends BaseService
{
    /**
     * 商品采集记录
     * @var GoodsCollectorModel|null
     */
    private ?GoodsCollectorModel $record = null;

    /**
     * 错误日志记录
     * @var array
     */
    private array $errorLog = [];

    /**
     * 采集成功的数量
     * @var int
     */
    private int $successCount = 0;

    /**
     * 图片url临时记录 (同一个网络图片仅入库一次)
     * @var array
     */
    private array $dictImageUrls = [];

    /**
     * 批量采集商品
     * @param array $urls 采集的商品url集
     * @param array $form 用户提交的表单
     * @param int $recordId 采集记录id
     * @param int $storeId 当前商城id
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function batch(array $urls, array $form, int $recordId, int $storeId): bool
    {
        // 检查采集记录状态是否异常
        if (!$this->checkStatusFail($recordId)) {
            foreach ($urls as $url) {
                try {
                    // 采集第三方商品数据
                    $original = $this->collector($url, $storeId);
                    // 下载远程商品图片
                    $original = $this->thirdPartyImages($original, $form['imageStorage'], $storeId);
                } catch (\Throwable $e) {
                    tre($e->getTraceAsString());
                    $this->errorLog[] = ['url' => trim($url), 'message' => $e->getMessage()];
                    continue;
                }
                // 生成商品数据(用于写入数据库)
                $data = $this->createData($original, $form, $storeId);
                // 事务处理:添加商品
                $model = new GoodsModel();
                $model->transaction(function () use ($model, $data, $storeId) {
                    // 添加商品
                    $model->save($data);
                    // 新增商品与分类关联
                    GoodsCategoryRelModel::increased((int)$model['goods_id'], $data['categoryIds'], $storeId);
                    // 新增商品与图片关联
                    GoodsImageModel::increased((int)$model['goods_id'], $data['imagesIds'], $storeId);
                    // 新增商品与规格关联
                    GoodsSpecRelModel::increased((int)$model['goods_id'], $data['newSpecList'], $storeId);
                    // 新增商品sku信息
                    GoodsSkuModel::add((int)$model['goods_id'], $data['spec_type'], $data['newSkuList'], $storeId);
                });
                // 记录采集成功
                $this->successCount++;
            }
        }
        // 更新采集记录
        $this->updateRecord($recordId, \count($urls));
        return true;
    }

    /**
     * 检查采集记录状态是否异常
     * @param int $recordId
     * @return bool
     */
    private function checkStatusFail(int $recordId): bool
    {
        $model = $this->getRecord($recordId);
        return $model['status'] == GoodsCollectorStatusEnum::FAIL || $model['is_delete'];
    }

    /**
     * 抓取第三方商品内容
     * @param string $url
     * @param int $storeId
     * @return array
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function collector(string $url, int $storeId): array
    {
        // 获取商品来源
        $store = $this->getStore($url);
        // 获取url中的商品ID
        $itemId = $this->getItemId($url, $store);
        // 商城采集设置
        $config = SettingModel::getItem(SettingEnum::COLLECTOR, $storeId);
        // 请求API查询商品详情
        return CollectorFacade::store($store)
            ->setOptions($config['config'][$config['provider']])
            ->detail($itemId);
    }

    /**
     * 根据url获取商品来源
     * @param string $url
     * @return string
     * @throws BaseException
     */
    private function getStore(string $url): string
    {
        $UrlStore = new UrlStore($url);
        if (!$store = $UrlStore->getStore()) {
            throwError('不支持该网址的采集,请检查url是否正确');
        }
        return $store;
    }

    /**
     * 获取url中的商品ID
     * @param string $url
     * @param string $store
     * @return string
     * @throws BaseException
     */
    private function getItemId(string $url, string $store): string
    {
        $UrlItemId = new UrlItemId($url, $store);
        if (!$itemId = $UrlItemId->getItemId()) {
            throwError('未能获取到商品ID,请检查url是否正确');
        }
        return $itemId;
    }

    /**
     * 更新采集记录
     * @param int $recordId 商品采集记录ID
     * @param int $currentCount 当前任务采集的商品总量
     * @return void
     */
    private function updateRecord(int $recordId, int $currentCount)
    {
        // 获取采集记录
        $model = $this->getRecord($recordId);
        // 计算采集失败的数量
        $failCount = $currentCount - $this->successCount;
        // 更新采集记录
        $model->save([
            'success_count' => $model['success_count'] + $this->successCount,
            'fail_count' => $model['fail_count'] + $failCount,
            'fail_log' => array_merge($model['fail_log'], $this->errorLog),
        ]);
        // 判断是否为最后一次队列
        if ($model['total_count'] <= ($model['success_count'] + $model['fail_count'])) {
            $model->save(['end_time' => \time(), 'status' => GoodsCollectorStatusEnum::COMPLETED]);
        }
    }

    /**
     * 获取采集记录
     * @param int $recordId 商品采集记录ID
     * @return GoodsCollectorModel|array|null
     */
    private function getRecord(int $recordId)
    {
        if (!$this->record) {
            $this->record = GoodsCollectorModel::detail($recordId);
        }
        return $this->record;
    }

    /**
     * 生成商品数据(用于写入数据库)
     * @param array $original 商品原始数据
     * @param array $form 用户提交的表单
     * @param int $storeId 当前商城id
     * @return array
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function createData(array $original, array $form, int $storeId): array
    {
        // 整理商品数据
        $data = [
            'goods_type' => $form['goods_type'],
            'goods_name' => $original['goods_name'],
            'spec_type' => $original['spec_type'],
            'delivery_id' => $form['delivery_id'] ?? 0,
            'content' => $original['content'] ?? '',
            'sort' => 100,
            'deduct_stock_type' => DeductStockTypeEnum::CREATE,
            'status' => $form['goods_status'],
            'imagesIds' => $original['imagesIds'],
            'categoryIds' => $form['categoryIds'],
            // 下面是默认数据, 没有会报错
            'alone_grade_equity' => [],
            'newSpecList' => [],
            'newSkuList' => [],
            'store_id' => $storeId,
        ];
        // 整理商品的价格和库存总量
        if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) {
            $data['stock_total'] = GoodsSkuModel::getStockTotal($original['specData']['skuList']);
            [$data['goods_price_min'], $data['goods_price_max']] = GoodsSkuModel::getGoodsPrices($original['specData']['skuList']);
            [$data['line_price_min'], $data['line_price_max']] = GoodsSkuModel::getLinePrices($original['specData']['skuList']);
        } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) {
            $data['goods_price_min'] = $data['goods_price_max'] = $original['goods_price'];
            $data['line_price_min'] = $data['line_price_max'] = $original['line_price'];
            $data['stock_total'] = $original['stock_num'];
        }
        // 规格和sku数据处理
        if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) {
            // 验证规格值是否合法
            SpecModel::checkSpecData($original['specData']['specList']);
            // 生成多规格数据 (携带id)
            $data['newSpecList'] = SpecModel::getNewSpecList($original['specData']['specList'], $storeId);
            // 生成skuList (携带goods_sku_id)
            $data['newSkuList'] = GoodsSkuModel::getNewSkuList($data['newSpecList'], $original['specData']['skuList']);
        } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) {
            // 生成skuItem
            $data['newSkuList'] = helper::pick($original, ['goods_price', 'line_price', 'stock_num', 'goods_weight']);
        }
        return $data;
    }

    /**
     * 下载远程商品图片
     * @param array $original 商品信息
     * @param int $imageStorage 商品主图 (10下载到本地、20使用源图片url)
     * @param int $storeId
     * @return array
     * @throws BaseException
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function thirdPartyImages(array $original, int $imageStorage, int $storeId): array
    {
        // 保存商品主图
        $original['imagesIds'] = [];
        foreach ($original['goodsImages'] as $imageUrl) {
            $original['imagesIds'][] = $imageStorage === 10 ? $this->downImage($imageUrl, $storeId)
                : $this->externalImage($imageUrl, $storeId);
        }
        // 保存商品SKU封面图
        if ($original['spec_type'] === GoodsSpecTypeEnum::MULTI) {
            foreach ($original['specData']['skuList'] as &$skuItem) {
                if (!empty($skuItem['imageUrl'])) {
                    $skuItem['image_id'] = $imageStorage === 10 ? $this->downImage($skuItem['imageUrl'], $storeId)
                        : $this->externalImage($skuItem['imageUrl'], $storeId);
                }
            }
        }
        return $original;
    }

    /**
     * 将远程图片外链保存到文件库中
     * @param string $imageUrl 远程图片url
     * @param int $storeId
     * @return mixed
     * @throws BaseException
     */
    private function externalImage(string $imageUrl, int $storeId)
    {
        // 如果已存在该文件则直接返回文件ID
        if (isset($this->dictImageUrls[$imageUrl])) {
            return $this->dictImageUrls[$imageUrl];
        }
        // 文件外链记录到文件库
        $UploadService = (new UploadService)->setStoreId($storeId);
        if (!$UploadService->uploadByExternal(FileTypeEnum::IMAGE, $imageUrl)) {
            throwError('图片记录失败:' . $UploadService->getError());
        }
        // 记录文件ID
        $fileId = $UploadService->getFileInfo()['file_id'];
        $this->dictImageUrls[$imageUrl] = $fileId;
        return $fileId;
    }

    /**
     * 将远程图片下载并保存到文件库中
     * @param string $imageUrl 远程图片url
     * @param int $storeId
     * @return mixed
     * @throws BaseException
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function downImage(string $imageUrl, int $storeId)
    {
        // 如果已存在该文件则直接返回文件ID
        if (isset($this->dictImageUrls[$imageUrl])) {
            return $this->dictImageUrls[$imageUrl];
        }
        // 下载远程图片
        $filePath = (new Download)->saveTempImage($storeId, $imageUrl);
        // 文件上传到文件库
        $UploadService = (new UploadService)->setStoreId($storeId);
        if (!$UploadService->uploadByLocal(FileTypeEnum::IMAGE, $filePath)) {
            throwError('图片上传失败:' . $UploadService->getError());
        }
        // 记录文件ID
        $fileId = $UploadService->getFileInfo()['file_id'];
        $this->dictImageUrls[$imageUrl] = $fileId;
        return $fileId;
    }
}