<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Tools\Entity;

/**
 * 传输无关的 ERP 实体引用。只承载身份，不包含 URL、路由名或模型类名。
 */
final readonly class EntityReference
{
    public const MAX_LABEL_LENGTH = 255;

    /**
     * @var list<string>
     */
    private const ALLOWED_KEYS = ['entity_type', 'entity_id', 'label'];

    /**
     * @var list<string>
     */
    private const FORBIDDEN_KEYS = ['url', 'href', 'route', 'route_name', 'path', 'model', 'class'];

    public function __construct(
        public EntityType $entityType,
        public int $entityId,
        public string $label,
    ) {}

    /**
     * @return array{entity_type: string, entity_id: int, label: string}
     */
    public function toArray(): array
    {
        return [
            'entity_type' => $this->entityType->value,
            'entity_id' => $this->entityId,
            'label' => $this->label,
        ];
    }

    public function key(): string
    {
        return $this->entityType->value.':'.$this->entityId;
    }

    public static function tryMake(EntityType $type, mixed $id, ?string $code = null, ?string $name = null): ?self
    {
        if (! is_numeric($id) || (int) $id <= 0) {
            return null;
        }

        $label = self::labelFrom($code, $name);
        if ($label === '') {
            $label = $type->value.' #'.(int) $id;
        }

        return new self($type, (int) $id, $label);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    public static function attach(array $payload, EntityType $type, mixed $id, ?string $code = null, ?string $name = null): array
    {
        $reference = self::tryMake($type, $id, $code, $name);

        if ($reference instanceof self) {
            $payload['entity_ref'] = $reference->toArray();
        }

        return $payload;
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    public static function attachMany(array $payload, EntityReference ...$references): array
    {
        $items = [];

        foreach ($references as $reference) {
            $items[$reference->key()] = $reference->toArray();
        }

        if ($items !== []) {
            $payload['entity_refs'] = array_values($items);
        }

        return $payload;
    }

    /**
     * @param  array<string, mixed>  $identity
     * @return array<string, mixed>
     */
    public static function attachNested(array $identity, EntityType $type): array
    {
        return self::attach(
            $identity,
            $type,
            $identity['id'] ?? null,
            isset($identity['code']) ? (string) $identity['code'] : null,
            isset($identity['name']) ? (string) $identity['name'] : null,
        );
    }

    public static function tryFromArray(mixed $value): ?self
    {
        if (! is_array($value)) {
            return null;
        }

        foreach (array_keys($value) as $key) {
            if (! is_string($key) || in_array($key, self::FORBIDDEN_KEYS, true) || ! in_array($key, self::ALLOWED_KEYS, true)) {
                return null;
            }
        }

        $type = EntityType::tryFromMixed($value['entity_type'] ?? null);
        $id = $value['entity_id'] ?? null;
        $label = $value['label'] ?? null;

        if (! $type instanceof EntityType || ! is_numeric($id) || (int) $id <= 0) {
            return null;
        }

        if (! is_string($label)) {
            return null;
        }

        $normalizedLabel = trim($label);

        if ($normalizedLabel === '' || mb_strlen($normalizedLabel) > self::MAX_LABEL_LENGTH) {
            return null;
        }

        return new self($type, (int) $id, $normalizedLabel);
    }

    private static function labelFrom(?string $code, ?string $name): string
    {
        $parts = [];

        foreach ([$code, $name] as $part) {
            if ($part === null) {
                continue;
            }

            $normalized = trim($part);

            if ($normalized !== '') {
                $parts[] = $normalized;
            }
        }

        $label = trim(implode(' ', $parts));

        if (mb_strlen($label) > self::MAX_LABEL_LENGTH) {
            return mb_substr($label, 0, self::MAX_LABEL_LENGTH);
        }

        return $label;
    }
}
