<?php

declare(strict_types=1);

namespace App\Business\Integration;

use App\Business\Integration\Exceptions\IntegrationGovernanceException;
use App\Business\Integration\Enums\IntegrationMappingStatus;
use App\Business\Integration\Enums\IntegrationObjectType;
use App\Models\DberpCustomer;
use App\Models\DberpIntegrationMapping;
use App\Models\DberpProduct;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;

final class IntegrationMappingService
{
    public function findMapping(string $sourceSystem, IntegrationObjectType $objectType, string $externalId): ?DberpIntegrationMapping
    {
        return DberpIntegrationMapping::query()
            ->where('source_system', $sourceSystem)
            ->where('object_type', $objectType->value)
            ->where('external_id', $externalId)
            ->first();
    }

    public function upsertMapping(
        string $sourceSystem,
        IntegrationObjectType $objectType,
        string $externalId,
        Model $localModel,
        array $attributes = [],
        IntegrationMappingStatus $status = IntegrationMappingStatus::Synced,
        ?string $matchStrategy = null,
    ): DberpIntegrationMapping {
        $mapping = DberpIntegrationMapping::query()->firstOrNew([
            'source_system' => $sourceSystem,
            'object_type' => $objectType->value,
            'external_id' => $externalId,
        ]);

        $mapping->fill(array_merge($attributes, [
            'target_system' => 'dberp',
            'local_model_type' => $localModel->getMorphClass(),
            'local_model_id' => $localModel->getKey(),
            'status' => $status->value,
            'match_strategy' => $matchStrategy,
            'governance_note' => null,
            'last_error_type' => null,
            'last_error_message' => null,
            'last_synced_at' => now(),
            'invalidated_at' => null,
        ]));
        $mapping->save();

        return $mapping;
    }

    public function upsertPlaceholderMapping(
        string $sourceSystem,
        IntegrationObjectType $objectType,
        string $externalId,
        array $attributes = [],
        IntegrationMappingStatus $status = IntegrationMappingStatus::Failed,
        ?string $matchStrategy = null,
    ): DberpIntegrationMapping {
        $mapping = DberpIntegrationMapping::query()->firstOrNew([
            'source_system' => $sourceSystem,
            'object_type' => $objectType->value,
            'external_id' => $externalId,
        ]);

        $mapping->fill(array_merge($attributes, [
            'target_system' => 'dberp',
            'local_model_type' => null,
            'local_model_id' => null,
            'status' => $status->value,
            'match_strategy' => $matchStrategy,
            'last_synced_at' => now(),
            'invalidated_at' => $status === IntegrationMappingStatus::Invalid ? now() : null,
        ]));
        $mapping->save();

        return $mapping;
    }

    public function remap(DberpIntegrationMapping $mapping, Model $localModel, string $matchStrategy = 'manual'): DberpIntegrationMapping
    {
        $occupiedMapping = $this->findMappingByLocalModel($mapping->source_system, $mapping->object_type, $localModel, (int) $mapping->getKey());

        if ($occupiedMapping instanceof DberpIntegrationMapping) {
            $this->markInvalid($occupiedMapping, '原绑定已被新的人工重绑替代。', 'manual_rebound');
        }

        $mapping->forceFill([
            'local_model_type' => $localModel->getMorphClass(),
            'local_model_id' => $localModel->getKey(),
            'status' => IntegrationMappingStatus::Matched->value,
            'match_strategy' => $matchStrategy,
            'governance_note' => '已人工重新绑定。',
            'last_error_type' => null,
            'last_error_message' => null,
            'last_synced_at' => now(),
            'invalidated_at' => null,
        ])->save();

        return $mapping;
    }

    public function markConflict(DberpIntegrationMapping $mapping, string $message, ?string $errorType = 'mapping_conflict'): DberpIntegrationMapping
    {
        $mapping->forceFill([
            'status' => IntegrationMappingStatus::Conflict->value,
            'governance_note' => $message,
            'last_error_type' => $errorType,
            'last_error_message' => $message,
            'invalidated_at' => null,
            'last_synced_at' => now(),
        ])->save();

        return $mapping;
    }

    public function markFailed(DberpIntegrationMapping $mapping, string $message, ?string $errorType = 'sync_failed'): DberpIntegrationMapping
    {
        $mapping->forceFill([
            'status' => IntegrationMappingStatus::Failed->value,
            'governance_note' => $message,
            'last_error_type' => $errorType,
            'last_error_message' => $message,
            'invalidated_at' => null,
            'last_synced_at' => now(),
        ])->save();

        return $mapping;
    }

    public function markInvalid(DberpIntegrationMapping $mapping, string $message, ?string $errorType = 'mapping_invalidated'): DberpIntegrationMapping
    {
        $mapping->forceFill([
            'status' => IntegrationMappingStatus::Invalid->value,
            'governance_note' => $message,
            'last_error_type' => $errorType,
            'last_error_message' => $message,
            'invalidated_at' => now(),
        ])->save();

        return $mapping;
    }

    public function findMappingByLocalModel(string $sourceSystem, string $objectType, Model $localModel, ?int $ignoreId = null): ?DberpIntegrationMapping
    {
        return DberpIntegrationMapping::query()
            ->where('source_system', $sourceSystem)
            ->where('object_type', $objectType)
            ->where('local_model_type', $localModel->getMorphClass())
            ->where('local_model_id', $localModel->getKey())
            ->when($ignoreId !== null, fn ($query) => $query->whereKeyNot($ignoreId))
            ->orderBy('id')
            ->first();
    }

    public function matchProductCandidates(string $externalId, ?string $externalSku = null, ?string $barcode = null, ?string $externalCode = null, ?string $name = null): EloquentCollection
    {
        return $this->matchCandidates(DberpProduct::query(), [
            'external_product_id' => $externalId,
            'external_sku' => $externalSku,
            'barcode' => $barcode,
            'code' => $externalCode,
            'name' => $name,
        ]);
    }

    public function matchCustomerCandidates(string $externalId, ?string $mobile = null, ?string $name = null): EloquentCollection
    {
        return $this->matchCandidates(DberpCustomer::query(), [
            'external_member_id' => $externalId,
            'mobile' => $mobile,
            'name' => $name,
        ]);
    }

    public function resolveProduct(string $sourceSystem, string $externalId, ?string $externalSku = null, ?string $barcode = null, ?string $externalCode = null, ?string $name = null): ?DberpProduct
    {
        $mapping = $this->findMapping($sourceSystem, IntegrationObjectType::Product, $externalId);

        if ($mapping?->localModel instanceof DberpProduct) {
            return $mapping->localModel;
        }

        $candidates = $this->matchProductCandidates($externalId, $externalSku, $barcode, $externalCode, $name);

        return $candidates->count() === 1 ? $candidates->first() : null;
    }

    public function resolveCustomer(string $sourceSystem, string $externalId, ?string $mobile = null, ?string $name = null): ?DberpCustomer
    {
        $mapping = $this->findMapping($sourceSystem, IntegrationObjectType::Customer, $externalId);

        if ($mapping?->localModel instanceof DberpCustomer) {
            return $mapping->localModel;
        }

        $candidates = $this->matchCustomerCandidates($externalId, $mobile, $name);

        return $candidates->count() === 1 ? $candidates->first() : null;
    }

    public function ensureSingleCandidate(string $objectLabel, EloquentCollection $candidates): Model
    {
        if ($candidates->count() !== 1) {
            throw new IntegrationGovernanceException('mapping_conflict', '存在多个可匹配的'.$objectLabel.'，请先人工处理映射。');
        }

        return $candidates->firstOrFail();
    }

    private function matchCandidates($query, array $conditions): EloquentCollection
    {
        $filteredConditions = array_filter($conditions, static fn ($value): bool => $value !== null && $value !== '');

        if ($filteredConditions === []) {
            return $query->whereRaw('1 = 0')->get();
        }

        $isFirst = true;

        foreach ($filteredConditions as $column => $value) {
            $method = $isFirst ? 'where' : 'orWhere';
            $query->{$method}($column, $value);
            $isFirst = false;
        }

        return $query->orderBy('id')->get();
    }
}