<?php
declare(strict_types=1);

/**
 * 数据单明细模型，对应 wash_sheet_item 表
 * 明细的新增/修改/删除都会把所属数据单的更新时间同步刷新（WashSheet::touch()）
 *
 * 两种使用场景：
 *   1. 主菜单「数据单明细」：不限定数据单，全表查改
 *   2. 数据单详情页：先 scopeToSheet($sheetId)，之后查/增/改/删都锁在这张单里
 */
class WashSheetItem extends Model
{
    protected string $table = 'wash_sheet_item';
    protected string $key   = 'wash_sheet_item_id';

    /** 粘贴导入时没写厂商的号码，默认归到这个厂商 */
    public const DEFAULT_VENDOR_NAME = 'OE';

    /** 号码字段长度上限，与 wash_sheet_item_number 的 VARCHAR(50) 保持一致 */
    private const NUMBER_MAX_LENGTH = 50;

    /** 单条 INSERT 语句最多带多少行，粘贴几千条时分批写库 */
    private const INSERT_CHUNK = 500;

    /** 限定的数据单单号，0 表示不限定 */
    private int $scopeSheetId = 0;

    /**
     * 把后续所有读写限定在某张数据单内，由 Action 按请求参数决定
     */
    public function scopeToSheet(int $sheetId): void
    {
        $this->scopeSheetId = max(0, $sheetId);
    }

    public function grid(array $loadOptions): array
    {
        if ($this->scopeSheetId > 0) {
            $scope = ['wash_sheet_item_lnk_sheet_id', '=', $this->scopeSheetId];
            $loadOptions['filter'] = empty($loadOptions['filter'])
                ? $scope
                : [$loadOptions['filter'], 'and', $scope];
        }

        $result = parent::grid($loadOptions);

        // 清洗结果在库里是 JSON 字符串，前端列模板要按对象读，这里统一解开
        foreach ($result['data'] as &$row) {
            $row['wash_sheet_item_tcd'] = $this->decodeResult($row['wash_sheet_item_tcd'] ?? null);
            $row['wash_sheet_item_epc'] = $this->decodeResult($row['wash_sheet_item_epc'] ?? null);
        }

        return $result;
    }

    public function gridInsert(array $values): mixed
    {
        $this->stripReadOnly($values);
        $this->applyNumberFormat($values);
        if ($this->scopeSheetId > 0) {
            $values['wash_sheet_item_lnk_sheet_id'] = $this->scopeSheetId;
        }

        $itemId = parent::gridInsert($values);
        $this->touchSheets([(int) ($values['wash_sheet_item_lnk_sheet_id'] ?? 0)]);

        return $itemId;
    }

    public function gridUpdate(mixed $key, array $values): int
    {
        $this->stripReadOnly($values);
        $this->applyNumberFormat($values);

        $currentSheetId = $this->sheetIdOf((int) $key);
        if (!$this->inScope($currentSheetId)) {
            return 0;
        }

        if ($this->scopeSheetId > 0) {
            $values['wash_sheet_item_lnk_sheet_id'] = $this->scopeSheetId;
        }

        // 明细可能被改挂到另一张数据单上，改之前的归属和改之后的归属都要刷新更新时间
        $result = $values ? parent::gridUpdate($key, $values) : 0;
        $this->touchSheets([$currentSheetId, (int) ($values['wash_sheet_item_lnk_sheet_id'] ?? 0)]);

        return $result;
    }

    public function gridRemove(mixed $key): int
    {
        $sheetId = $this->sheetIdOf((int) $key);
        if (!$this->inScope($sheetId)) {
            return 0;
        }

        $result = parent::gridRemove($key);
        $this->touchSheets([$sheetId]);

        return $result;
    }

    /**
     * 删除某张数据单下的全部明细，数据单被删除时级联调用
     */
    public function removeBySheet(int $sheetId): int
    {
        if ($sheetId <= 0) {
            return 0;
        }

        return $this->db->delete($this->table, ['wash_sheet_item_lnk_sheet_id' => $sheetId])->rowCount();
    }

    /**
     * 粘贴导入：一行一条号码，只把当前数据单里还没有的号码写进去
     *   格式1  OE号            → 厂商取 DEFAULT_VENDOR_NAME
     *   格式2  厂商:号码       → 冒号（半角/全角均可）前是厂商名称，后是号码
     * 厂商必须是「号码厂商」里已存在的，未登记的厂商整行跳过并回报，不自动建主数据
     *
     * @return array{added:int,existed:int,repeated:int,failed:array<int,array{line:string,reason:string}>}
     */
    public function import(int $sheetId, string $text): array
    {
        $vendorModel = new NumberVendor($this->db);
        $vendorIds = $vendorModel->idsByName();
        $defaultKey = mb_strtolower(self::DEFAULT_VENDOR_NAME);
        // 默认厂商是系统内置的，首次导入时如果「号码厂商」里还没有就补一条
        $vendorIds[$defaultKey] ??= $vendorModel->ensureByName(self::DEFAULT_VENDOR_NAME);
        $defaultVendorId = $vendorIds[$defaultKey];

        $existing = $this->existingKeys($sheetId);
        $result = ['added' => 0, 'existed' => 0, 'repeated' => 0, 'failed' => []];
        $seen = [];
        $rows = [];

        foreach (preg_split('/\R/u', $text) ?: [] as $rawLine) {
            $line = trim($rawLine);
            if ($line === '') {
                continue;
            }

            $parsed = $this->parseLine($line, $vendorIds, $defaultVendorId);
            if (isset($parsed['reason'])) {
                $result['failed'][] = ['line' => $line, 'reason' => $parsed['reason']];
                continue;
            }

            $mapKey = $parsed['factoryId'] . "\0" . mb_strtolower($parsed['number']);
            if (isset($existing[$mapKey])) {
                $result['existed']++;
                continue;
            }
            if (isset($seen[$mapKey])) {
                $result['repeated']++;
                continue;
            }
            $seen[$mapKey] = true;

            $rows[] = [
                'wash_sheet_item_lnk_sheet_id'   => $sheetId,
                'wash_sheet_item_lnk_factory_id' => $parsed['factoryId'],
                'wash_sheet_item_number'         => $parsed['number'],
                'wash_sheet_item_format'         => formatNum($parsed['number']),
            ];
        }

        foreach (array_chunk($rows, self::INSERT_CHUNK) as $chunk) {
            $this->db->insert($this->table, $chunk);
            $result['added'] += count($chunk);
        }

        if ($result['added'] > 0) {
            $this->touchSheets([$sheetId]);
        }

        return $result;
    }

    /**
     * 清洗一条明细：TCD 和 EPC 各调一次 ypAPI，结果摘要写回本行
     * 逐条清洗是接口决定的——tcd/checkNumberData 传多个号码时结果是合并返回的，拆不回单条
     *
     * @param bool $force true=重新清洗；false=仅处理未清洗部分，已清洗过的直接跳过
     * @return array{key:int,skipped:bool,tcd:array|null,epc:array|null,cleanTime:string|null}
     * @throws MvcException
     */
    public function clean(int $itemId, bool $force): array
    {
        $row = $this->db->get($this->table, '*', [$this->key => $itemId]);
        if (!$row || !$this->inScope((int) $row['wash_sheet_item_lnk_sheet_id'])) {
            throw new MvcException(lg('明细不存在'));
        }

        if (!$force && !empty($row['wash_sheet_item_clean_time'])) {
            return [
                'key'       => $itemId,
                'skipped'   => true,
                'tcd'       => $this->decodeResult($row['wash_sheet_item_tcd'] ?? null),
                'epc'       => $this->decodeResult($row['wash_sheet_item_epc'] ?? null),
                'cleanTime' => (string) $row['wash_sheet_item_clean_time'],
            ];
        }

        $factory = (new NumberVendor($this->db))->nameById((int) $row['wash_sheet_item_lnk_factory_id']);
        $number  = (string) $row['wash_sheet_item_number'];

        $result = Wash::fromConfig()->cleanNumber($factory, $number);
        $tcd = $result['tcd'];
        $epc = $result['epc'];

        $cleanTime = date('Y-m-d H:i:s');
        $this->db->update($this->table, [
            'wash_sheet_item_tcd'        => json_encode($tcd, JSON_UNESCAPED_UNICODE),
            'wash_sheet_item_epc'        => json_encode($epc, JSON_UNESCAPED_UNICODE),
            'wash_sheet_item_clean_time' => $cleanTime,
        ], [$this->key => $itemId]);

        $this->touchSheets([(int) $row['wash_sheet_item_lnk_sheet_id']]);

        return ['key' => $itemId, 'skipped' => false, 'tcd' => $tcd, 'epc' => $epc, 'cleanTime' => $cleanTime];
    }

    /**
     * 清洗结果列在库里是 JSON 字符串，grid 要的是对象，统一在这里解
     */
    private function decodeResult(?string $json): ?array
    {
        if ($json === null || $json === '') {
            return null;
        }

        $decoded = json_decode($json, true);

        return is_array($decoded) ? $decoded : null;
    }

    /**
     * 解析一行粘贴内容
     * @param array<string,int> $vendorIds 厂商名称（小写）→ ID
     * @return array{factoryId:int,number:string}|array{reason:string}
     */
    private function parseLine(string $line, array $vendorIds, int $defaultVendorId): array
    {
        $factoryId = $defaultVendorId;
        $number = $line;

        if (preg_match('/^([^:：]*)[:：](.*)$/u', $line, $matches)) {
            $vendorName = trim($matches[1]);
            $number = trim($matches[2]);

            if ($vendorName === '' || $number === '') {
                return ['reason' => lg('格式不正确')];
            }

            $factoryId = $vendorIds[mb_strtolower($vendorName)] ?? 0;
            if ($factoryId === 0) {
                return ['reason' => lg('号码厂商不存在')];
            }
        }

        if (mb_strlen($number) > self::NUMBER_MAX_LENGTH) {
            return ['reason' => lg('号码过长')];
        }

        return ['factoryId' => $factoryId, 'number' => $number];
    }

    /**
     * 当前数据单里已有的「厂商+号码」集合，导入时用来判断哪些号码已存在
     * @return array<string,true>
     */
    private function existingKeys(int $sheetId): array
    {
        $rows = $this->db->select($this->table, [
            'wash_sheet_item_lnk_factory_id',
            'wash_sheet_item_number',
        ], ['wash_sheet_item_lnk_sheet_id' => $sheetId]);

        $keys = [];
        foreach ($rows as $row) {
            $keys[$row['wash_sheet_item_lnk_factory_id'] . "\0" . mb_strtolower($row['wash_sheet_item_number'])] = true;
        }

        return $keys;
    }

    /**
     * 限定了数据单时，只允许操作这张单下的明细
     */
    private function inScope(int $sheetId): bool
    {
        return $this->scopeSheetId === 0 || $sheetId === $this->scopeSheetId;
    }

    private function sheetIdOf(int $itemId): int
    {
        return (int) $this->db->get($this->table, 'wash_sheet_item_lnk_sheet_id', [$this->key => $itemId]);
    }

    /**
     * @param array<int> $sheetIds 去重后逐个刷新所属数据单的更新时间
     */
    private function touchSheets(array $sheetIds): void
    {
        $sheet = new WashSheet($this->db);
        foreach (array_unique(array_filter($sheetIds)) as $sheetId) {
            $sheet->touch($sheetId);
        }
    }

    /**
     * 后台自己维护的列一律不接受前端提交：时间由数据库或程序写，
     * 格式化号码由 applyNumberFormat() 生成，清洗结果只能由 clean() 写
     */
    private function stripReadOnly(array &$values): void
    {
        unset(
            $values['wash_sheet_item_create_time'],
            $values['wash_sheet_item_update_time'],
            $values['wash_sheet_item_format'],
            $values['wash_sheet_item_tcd'],
            $values['wash_sheet_item_epc'],
            $values['wash_sheet_item_clean_time']
        );
    }

    /**
     * 号码写库前同步生成格式化值（只留字母数字并转大写），单条新增/修改都走这里
     * 本次没有改号码时不动 format 字段，避免把已有值刷成空
     */
    private function applyNumberFormat(array &$values): void
    {
        if (array_key_exists('wash_sheet_item_number', $values)) {
            $values['wash_sheet_item_format'] = formatNum((string) $values['wash_sheet_item_number']);
        }
    }
}
