<?php

declare(strict_types=1);

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

namespace App\Business\Organization;

use App\Models\DberpDepartment;
use App\Models\DberpEmployee;
use App\Models\DberpPosition;
use App\Models\DberpSalesperson;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;

/**
 * 组织主数据：对职员/业务员在「启用主档名称唯一匹配」或「可从关联职员确定性带出」时的安全回填与治理扫描。
 */
final class OrganizationMasterDataBackfillService
{
    /**
     * @return array{
     *     dry_run: bool,
     *     report_path: string|null,
     *     employees: array{
     *         department_from_legacy_text: array{applied: int, skipped_already_set: int, ambiguous: list<array<string, mixed>>, unmatched: list<array<string, mixed>>},
     *         position_from_legacy_text: array{applied: int, skipped_already_set: int, ambiguous: list<array<string, mixed>>, unmatched: list<array<string, mixed>>}
     *     },
     *     salespersons: array{
     *         department_from_legacy_text: array{applied: int, skipped_already_set: int, ambiguous: list<array<string, mixed>>, unmatched: list<array<string, mixed>>},
     *         position_from_legacy_text: array{applied: int, skipped_already_set: int, ambiguous: list<array<string, mixed>>, unmatched: list<array<string, mixed>>},
     *         fk_aligned_from_employee: array{applied: int, skipped: int}
     *     }
     * }
     */
    public function run(bool $dryRun = true, bool $writeReport = false): array
    {
        $result = [
            'dry_run' => $dryRun,
            'report_path' => null,
            'employees' => [
                'department_from_legacy_text' => [
                    'applied' => 0,
                    'skipped_already_set' => 0,
                    'ambiguous' => [],
                    'unmatched' => [],
                ],
                'position_from_legacy_text' => [
                    'applied' => 0,
                    'skipped_already_set' => 0,
                    'ambiguous' => [],
                    'unmatched' => [],
                ],
            ],
            'salespersons' => [
                'department_from_legacy_text' => [
                    'applied' => 0,
                    'skipped_already_set' => 0,
                    'ambiguous' => [],
                    'unmatched' => [],
                ],
                'position_from_legacy_text' => [
                    'applied' => 0,
                    'skipped_already_set' => 0,
                    'ambiguous' => [],
                    'unmatched' => [],
                ],
                'fk_aligned_from_employee' => [
                    'applied' => 0,
                    'skipped' => 0,
                ],
            ],
        ];

        $this->scanEmployeesDepartmentLegacy($dryRun, $result);
        $this->scanEmployeesPositionLegacy($dryRun, $result);

        if (Schema::hasTable('dberp_salespersons') && Schema::hasColumn('dberp_salespersons', 'department_id')) {
            $this->scanSalespersonsDepartmentLegacy($dryRun, $result);
            $this->scanSalespersonsPositionLegacy($dryRun, $result);
            $this->alignSalespersonFksFromEmployee($dryRun, $result);
        }

        if ($writeReport) {
            $dir = storage_path('app/dberp-organization-master-data');
            if (! File::isDirectory($dir)) {
                File::makeDirectory($dir, 0755, true);
            }

            $path = $dir.'/governance-'.date('Y-m-d_His').'.json';
            File::put($path, json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
            $result['report_path'] = $path;
        }

        return $result;
    }

    /**
     * @param  array<string, mixed>  $result
     */
    private function scanEmployeesDepartmentLegacy(bool $dryRun, array &$result): void
    {
        $bucket = &$result['employees']['department_from_legacy_text'];

        DberpEmployee::query()
            ->whereNull('department_id')
            ->whereNotNull('department_name')
            ->whereRaw('TRIM(department_name) <> ?', [''])
            ->orderBy('id')
            ->each(function (DberpEmployee $employee) use ($dryRun, &$bucket): void {
                $label = trim((string) $employee->department_name);
                $resolution = $this->classifyUniqueEnabledDepartmentByExactName($label);

                if ($resolution['status'] === 'ambiguous') {
                    $bucket['ambiguous'][] = [
                        'entity' => 'employee',
                        'id' => $employee->getKey(),
                        'name' => $employee->name,
                        'legacy_text' => $label,
                        'match_count' => $resolution['match_count'],
                    ];

                    return;
                }

                if ($resolution['status'] === 'unmatched') {
                    $bucket['unmatched'][] = [
                        'entity' => 'employee',
                        'id' => $employee->getKey(),
                        'name' => $employee->name,
                        'legacy_text' => $label,
                    ];

                    return;
                }

                if ($resolution['department_id'] === null) {
                    return;
                }

                if (! $dryRun) {
                    $employee->forceFill(['department_id' => $resolution['department_id']])->saveQuietly();
                }

                $bucket['applied']++;
            });
    }

    /**
     * @param  array<string, mixed>  $result
     */
    private function scanEmployeesPositionLegacy(bool $dryRun, array &$result): void
    {
        $bucket = &$result['employees']['position_from_legacy_text'];

        DberpEmployee::query()
            ->whereNull('position_id')
            ->whereNotNull('position_name')
            ->whereRaw('TRIM(position_name) <> ?', [''])
            ->orderBy('id')
            ->each(function (DberpEmployee $employee) use ($dryRun, &$bucket): void {
                $label = trim((string) $employee->position_name);
                $resolution = $this->classifyUniqueEnabledPositionByExactName($label);

                if ($resolution['status'] === 'ambiguous') {
                    $bucket['ambiguous'][] = [
                        'entity' => 'employee',
                        'id' => $employee->getKey(),
                        'name' => $employee->name,
                        'legacy_text' => $label,
                        'match_count' => $resolution['match_count'],
                    ];

                    return;
                }

                if ($resolution['status'] === 'unmatched') {
                    $bucket['unmatched'][] = [
                        'entity' => 'employee',
                        'id' => $employee->getKey(),
                        'name' => $employee->name,
                        'legacy_text' => $label,
                    ];

                    return;
                }

                if ($resolution['position_id'] === null) {
                    return;
                }

                if (! $dryRun) {
                    $employee->forceFill(['position_id' => $resolution['position_id']])->saveQuietly();
                }

                $bucket['applied']++;
            });
    }

    /**
     * @param  array<string, mixed>  $result
     */
    private function scanSalespersonsDepartmentLegacy(bool $dryRun, array &$result): void
    {
        $bucket = &$result['salespersons']['department_from_legacy_text'];

        DberpSalesperson::query()
            ->whereNull('department_id')
            ->whereNotNull('department_text')
            ->whereRaw('TRIM(department_text) <> ?', [''])
            ->orderBy('id')
            ->each(function (DberpSalesperson $salesperson) use ($dryRun, &$bucket): void {
                $label = trim((string) $salesperson->department_text);
                $resolution = $this->classifyUniqueEnabledDepartmentByExactName($label);

                if ($resolution['status'] === 'ambiguous') {
                    $bucket['ambiguous'][] = [
                        'entity' => 'salesperson',
                        'id' => $salesperson->getKey(),
                        'legacy_text' => $label,
                        'match_count' => $resolution['match_count'],
                    ];

                    return;
                }

                if ($resolution['status'] === 'unmatched') {
                    $bucket['unmatched'][] = [
                        'entity' => 'salesperson',
                        'id' => $salesperson->getKey(),
                        'legacy_text' => $label,
                    ];

                    return;
                }

                if ($resolution['department_id'] === null) {
                    return;
                }

                if (! $dryRun) {
                    $salesperson->forceFill(['department_id' => $resolution['department_id']])->saveQuietly();
                }

                $bucket['applied']++;
            });
    }

    /**
     * @param  array<string, mixed>  $result
     */
    private function scanSalespersonsPositionLegacy(bool $dryRun, array &$result): void
    {
        $bucket = &$result['salespersons']['position_from_legacy_text'];

        DberpSalesperson::query()
            ->whereNull('position_id')
            ->whereNotNull('position_text')
            ->whereRaw('TRIM(position_text) <> ?', [''])
            ->orderBy('id')
            ->each(function (DberpSalesperson $salesperson) use ($dryRun, &$bucket): void {
                $label = trim((string) $salesperson->position_text);
                $resolution = $this->classifyUniqueEnabledPositionByExactName($label);

                if ($resolution['status'] === 'ambiguous') {
                    $bucket['ambiguous'][] = [
                        'entity' => 'salesperson',
                        'id' => $salesperson->getKey(),
                        'legacy_text' => $label,
                        'match_count' => $resolution['match_count'],
                    ];

                    return;
                }

                if ($resolution['status'] === 'unmatched') {
                    $bucket['unmatched'][] = [
                        'entity' => 'salesperson',
                        'id' => $salesperson->getKey(),
                        'legacy_text' => $label,
                    ];

                    return;
                }

                if ($resolution['position_id'] === null) {
                    return;
                }

                if (! $dryRun) {
                    $salesperson->forceFill(['position_id' => $resolution['position_id']])->saveQuietly();
                }

                $bucket['applied']++;
            });
    }

    /**
     * 关联职员存在时，将业务员上为空的 department_id / position_id 与职员主档对齐（确定性，非文本猜测）。
     *
     * @param  array<string, mixed>  $result
     */
    private function alignSalespersonFksFromEmployee(bool $dryRun, array &$result): void
    {
        $bucket = &$result['salespersons']['fk_aligned_from_employee'];

        DberpSalesperson::query()
            ->whereNotNull('employee_id')
            ->with('employee')
            ->orderBy('id')
            ->each(function (DberpSalesperson $salesperson) use ($dryRun, &$bucket): void {
                $employee = $salesperson->employee;
                if ($employee === null) {
                    $bucket['skipped']++;

                    return;
                }

                $updates = [];
                if ($salesperson->department_id === null && $employee->department_id !== null) {
                    $updates['department_id'] = $employee->department_id;
                }

                if ($salesperson->position_id === null && $employee->position_id !== null) {
                    $updates['position_id'] = $employee->position_id;
                }

                if ($updates === []) {
                    $bucket['skipped']++;

                    return;
                }

                if (! $dryRun) {
                    $salesperson->forceFill($updates)->saveQuietly();
                }

                $bucket['applied']++;
            });
    }

    /**
     * @return array{status: 'unique'|'ambiguous'|'unmatched', department_id: int|null, match_count: int}
     */
    private function classifyUniqueEnabledDepartmentByExactName(string $normalizedName): array
    {
        $count = DberpDepartment::query()
            ->where('is_enabled', true)
            ->where('name', $normalizedName)
            ->count();

        if ($count === 0) {
            return ['status' => 'unmatched', 'department_id' => null, 'match_count' => 0];
        }

        if ($count > 1) {
            return ['status' => 'ambiguous', 'department_id' => null, 'match_count' => $count];
        }

        $id = (int) DberpDepartment::query()
            ->where('is_enabled', true)
            ->where('name', $normalizedName)
            ->value('id');

        return ['status' => 'unique', 'department_id' => $id, 'match_count' => 1];
    }

    /**
     * @return array{status: 'unique'|'ambiguous'|'unmatched', position_id: int|null, match_count: int}
     */
    private function classifyUniqueEnabledPositionByExactName(string $normalizedName): array
    {
        $count = DberpPosition::query()
            ->where('is_enabled', true)
            ->where('name', $normalizedName)
            ->count();

        if ($count === 0) {
            return ['status' => 'unmatched', 'position_id' => null, 'match_count' => 0];
        }

        if ($count > 1) {
            return ['status' => 'ambiguous', 'position_id' => null, 'match_count' => $count];
        }

        $id = (int) DberpPosition::query()
            ->where('is_enabled', true)
            ->where('name', $normalizedName)
            ->value('id');

        return ['status' => 'unique', 'position_id' => $id, 'match_count' => 1];
    }
}
