<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\ServerResource;
use App\Models\User;
use App\Models\UserServerPermission;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

#[Apidoc\Title('用户管理')]
class UserController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api');
        $this->middleware('permission:platform.users.view,api')->only(['index', 'show']);
        $this->middleware('permission:platform.users.manage,api')->only(['store', 'update', 'destroy', 'syncPermissions', 'syncBatchAssignments', 'import', 'importTemplate']);
    }

    #[Apidoc\Title('用户列表'), Apidoc\Method('GET'), Apidoc\Url('/users')]
    public function index(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
            'sort_by' => ['nullable', 'string', 'in:id,nickname,email,phone,created_at'],
            'sort_order' => ['nullable', 'string', 'in:asc,desc'],
        ]);

        $sortBy = $validated['sort_by'] ?? 'created_at';
        $sortOrder = $validated['sort_order'] ?? 'desc';
        $perPage = (int) ($validated['per_page'] ?? 20);

        $users = User::query()
            ->with('roles')
            ->orderBy($sortBy, $sortOrder)
            ->paginate($perPage);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $users]);
    }

    #[Apidoc\Title('创建用户'), Apidoc\Method('POST'), Apidoc\Url('/users')]
    public function store(StoreUserRequest $request): JsonResponse
    {
        $user = User::query()->create($request->validated());

        if ($request->filled('role_ids')) {
            $user->syncRoles($request->validated('role_ids'));
        }

        $this->auditLog($request, 'user_create', ['metadata' => ['target_user_id' => $user->id]]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load('roles')], 201);
    }

    #[Apidoc\Title('用户详情'), Apidoc\Method('GET'), Apidoc\Url('/users/{id}')]
    public function show(int $id): JsonResponse
    {
        $user = User::query()->with(['roles', 'permissions', 'serverResources'])->findOrFail($id);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user]);
    }

    #[Apidoc\Title('更新用户'), Apidoc\Method('PUT'), Apidoc\Url('/users/{id}')]
    public function update(UpdateUserRequest $request, string $id): JsonResponse
    {
        if ($id === 'batch-assignments') {
            return $this->syncBatchAssignments($request);
        }

        $user = User::query()->findOrFail($id);
        $user->fill($request->safe()->except(['role_ids']));

        if ($request->filled('password')) {
            $user->password = $request->validated('password');
        }

        $user->save();

        if ($request->has('role_ids')) {
            $user->syncRoles($request->validated('role_ids'));
        }

        $this->auditLog($request, 'user_update', ['metadata' => ['target_user_id' => $user->id]]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load('roles')]);
    }

    #[Apidoc\Title('同步用户权限'), Apidoc\Method('PUT'), Apidoc\Url('/users/{id}/permissions')]
    public function syncPermissions(Request $request, int $id): JsonResponse
    {
        $validated = $request->validate([
            'permission_ids' => ['present', 'array'],
            'permission_ids.*' => ['integer', 'exists:permissions,id'],
        ]);

        $user = User::query()->findOrFail($id);
        $user->syncPermissions($validated['permission_ids']);
        $this->syncServerResourcePermissionsByDirectPermissions($user, $validated['permission_ids']);

        $this->auditLog($request, 'user_permissions_update', ['metadata' => ['target_user_id' => $user->id, 'permission_ids' => $validated['permission_ids']]]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load(['roles', 'permissions'])]);
    }

    #[Apidoc\Title('批量设置用户组和用户权限'), Apidoc\Method('PUT'), Apidoc\Url('/users/batch-assignments')]
    public function syncBatchAssignments(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'user_ids' => ['required', 'array', 'min:1'],
            'user_ids.*' => ['integer', 'exists:users,id'],
            'role_ids' => ['present', 'array'],
            'role_ids.*' => ['integer', 'exists:roles,id'],
            'permission_ids' => ['present', 'array'],
            'permission_ids.*' => ['integer', 'exists:permissions,id'],
        ]);

        $userIds = collect($validated['user_ids'])->map(fn (int $id): int => (int) $id)->unique()->values()->all();
        $roleIds = collect($validated['role_ids'])->map(fn (int $id): int => (int) $id)->unique()->values()->all();
        $permissionIds = collect($validated['permission_ids'])->map(fn (int $id): int => (int) $id)->unique()->values()->all();

        $users = User::query()->whereIn('id', $userIds)->get();
        foreach ($users as $user) {
            $user->syncRoles($roleIds);
            $user->syncPermissions($permissionIds);
            $this->syncServerResourcePermissionsByDirectPermissions($user, $permissionIds);
        }

        $this->auditLog($request, 'users_batch_assignments_update', [
            'metadata' => [
                'target_user_ids' => $userIds,
                'role_ids' => $roleIds,
                'permission_ids' => $permissionIds,
            ],
        ]);

        return response()->json([
            'code' => 0,
            'message' => 'ok',
            'data' => [
                'updated_count' => $users->count(),
            ],
        ]);
    }

    #[Apidoc\Title('删除用户'), Apidoc\Method('DELETE'), Apidoc\Url('/users/{id}')]
    public function destroy(Request $request, int $id): JsonResponse
    {
        $user = User::query()->findOrFail($id);
        $this->auditLog($request, 'user_delete', ['metadata' => ['target_user_id' => $user->id]]);
        $user->delete();

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
    }

    #[Apidoc\Title('批量导入用户'), Apidoc\Method('POST'), Apidoc\Url('/users/import')]
    public function import(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'file' => ['required', 'file', 'mimes:xlsx'],
        ]);

        $file = $validated['file'];
        $rows = $this->parseImportRows($file->getPathname(), (string) $file->getClientOriginalExtension());
        if ($rows->isEmpty()) {
            throw ValidationException::withMessages([
                'file' => ['文件中没有可导入的数据行。'],
            ]);
        }

        $createdCount = 0;
        $skippedCount = 0;
        $errors = [];

        foreach ($rows as $index => $row) {
            $line = $index + 2;
            $normalizedRow = [];
            foreach ($row as $key => $value) {
                $normalizedRow[$this->normalizeImportHeader((string) $key)] = (string) $value;
            }
            $payload = [
                'nickname' => trim($this->firstMatchedValue($normalizedRow, ['nickname', 'nick_name', 'name', '用户名', '昵称'])),
                'email' => trim($this->firstMatchedValue($normalizedRow, ['email', 'mail', '邮箱'])),
                'phone' => trim($this->firstMatchedValue($normalizedRow, ['phone', 'mobile', '手机号', '手机', '电话'])),
                'password' => trim($this->firstMatchedValue($normalizedRow, ['password', 'passwd', 'pwd', '密码'])),
                'force_password_change' => $this->parseBooleanValue($this->firstMatchedValue($normalizedRow, ['force_password_change', 'require_password_change', '要求更改密码', 'force_change_password'])),
            ];

            $validator = Validator::make($payload, [
                'nickname' => ['required', 'string', 'max:255'],
                'email' => ['required', 'email', 'max:255', 'unique:users,email'],
                'phone' => ['nullable', 'string', 'max:32', 'unique:users,phone'],
                'password' => ['required', 'string', 'min:6', 'max:255'],
            ]);

            if ($validator->fails()) {
                $errors[] = [
                    'line' => $line,
                    'message' => implode('；', $validator->errors()->all()),
                ];
                $skippedCount++;

                continue;
            }

            $user = User::query()->create($payload);
            $roleIds = $this->resolveRoleIds($normalizedRow);
            if (! empty($roleIds)) {
                $user->syncRoles($roleIds);
            }
            $createdCount++;
        }

        $this->auditLog($request, 'user_import', [
            'metadata' => [
                'created_count' => $createdCount,
                'skipped_count' => $skippedCount,
                'error_count' => count($errors),
            ],
        ]);

        return response()->json([
            'code' => 0,
            'message' => 'ok',
            'data' => [
                'created_count' => $createdCount,
                'skipped_count' => $skippedCount,
                'errors' => $errors,
            ],
        ]);
    }

    #[Apidoc\Title('下载用户导入模板'), Apidoc\Method('GET'), Apidoc\Url('/users/import/template')]
    public function importTemplate()
    {
        if (! class_exists(\ZipArchive::class)) {
            throw ValidationException::withMessages([
                'file' => ['当前 PHP 环境未启用 ZipArchive 扩展，无法生成 xlsx 模板。'],
            ]);
        }

        $tempPath = tempnam(sys_get_temp_dir(), 'users_import_template_');
        if ($tempPath === false) {
            throw ValidationException::withMessages(['file' => ['无法创建临时模板文件。']]);
        }

        $xlsxPath = $tempPath.'.xlsx';
        @rename($tempPath, $xlsxPath);
        $this->buildUsersImportTemplateXlsx($xlsxPath);

        return response()->download(
            $xlsxPath,
            'users_import_template.xlsx',
            ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
        )->deleteFileAfterSend(true);
    }

    private function parseImportRows(string $filePath, string $extension): Collection
    {
        $ext = strtolower($extension);
        if ($ext !== 'xlsx') {
            throw ValidationException::withMessages(['file' => ['仅支持 xlsx 文件导入。']]);
        }

        return $this->parseXlsxRows($filePath);
    }

    private function parseXlsxRows(string $filePath): Collection
    {
        if (! class_exists(\ZipArchive::class)) {
            throw ValidationException::withMessages([
                'file' => ['当前 PHP 环境未启用 ZipArchive 扩展，无法导入 xlsx。请启用 php_zip 扩展。'],
            ]);
        }

        $zip = new \ZipArchive;
        if ($zip->open($filePath) !== true) {
            throw ValidationException::withMessages(['file' => ['无法解析 xlsx 文件。']]);
        }

        $sharedStrings = [];
        $sharedXml = $zip->getFromName('xl/sharedStrings.xml');
        if (is_string($sharedXml)) {
            $sharedDoc = new \SimpleXMLElement($sharedXml);
            foreach ($sharedDoc->si as $si) {
                $sharedStrings[] = (string) $si->t;
            }
        }

        $sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
        $zip->close();
        if (! is_string($sheetXml)) {
            throw ValidationException::withMessages(['file' => ['xlsx 文件缺少 sheet1。']]);
        }

        $sheet = new \SimpleXMLElement($sheetXml);
        $rowsData = [];
        foreach ($sheet->sheetData->row as $rowNode) {
            $rowValues = [];
            foreach ($rowNode->c as $cell) {
                $ref = (string) $cell['r'];
                preg_match('/^[A-Z]+/', $ref, $matches);
                $column = $matches[0] ?? '';
                $value = '';
                if ((string) $cell['t'] === 's') {
                    $index = (int) ($cell->v ?? 0);
                    $value = (string) ($sharedStrings[$index] ?? '');
                } else {
                    $value = (string) ($cell->v ?? '');
                }
                $rowValues[$column] = $value;
            }
            $rowsData[] = $rowValues;
        }

        if (count($rowsData) <= 1) {
            return collect();
        }

        $headerRow = $rowsData[0];
        $orderedColumns = array_keys($headerRow);
        $headers = [];
        foreach ($orderedColumns as $col) {
            $headers[] = $this->normalizeImportHeader((string) ($headerRow[$col] ?? ''));
        }

        $resultRows = collect();
        for ($i = 1; $i < count($rowsData); $i++) {
            $row = $rowsData[$i];
            $assoc = [];
            $hasValue = false;
            foreach ($orderedColumns as $idx => $col) {
                $header = $headers[$idx] ?? '';
                if ($header === '') {
                    continue;
                }
                $value = (string) ($row[$col] ?? '');
                if (trim($value) !== '') {
                    $hasValue = true;
                }
                $assoc[$header] = $value;
            }
            if ($hasValue) {
                $resultRows->push($assoc);
            }
        }

        return $resultRows;
    }

    private function resolveRoleIds(array $row): array
    {
        $roleIdsRaw = trim((string) ($row['role_ids'] ?? ''));
        if ($roleIdsRaw !== '') {
            return collect(explode(',', $roleIdsRaw))
                ->map(fn ($id) => (int) trim($id))
                ->filter(fn (int $id): bool => $id > 0 && Role::query()->where('id', $id)->exists())
                ->values()
                ->all();
        }

        $roleNamesRaw = trim((string) ($row['roles'] ?? $row['role_names'] ?? ''));
        if ($roleNamesRaw === '') {
            return [];
        }

        $names = collect(explode(',', $roleNamesRaw))
            ->map(fn ($name) => trim($name))
            ->filter()
            ->values()
            ->all();

        if (empty($names)) {
            return [];
        }

        return Role::query()
            ->whereIn('name', $names)
            ->pluck('id')
            ->map(fn (int $id): int => (int) $id)
            ->all();
    }

    private function normalizeImportHeader(string $header): string
    {
        $normalized = trim($this->toUtf8($header));
        $normalized = preg_replace('/^\xEF\xBB\xBF/u', '', $normalized) ?? $normalized;
        $normalized = strtolower(str_replace([' ', '-', '.'], '_', trim($normalized)));

        return match ($normalized) {
            '昵称', '用户名', '姓名', 'name', 'nick_name' => 'nickname',
            '邮箱', '邮件', 'mail' => 'email',
            '手机号', '手机', '电话', 'mobile' => 'phone',
            '密码', 'passwd', 'pwd' => 'password',
            '角色id', '角色ids', 'role_id', '角色_id' => 'role_ids',
            '角色', '角色名', 'role_name', 'role_names' => 'roles',
            default => $normalized,
        };
    }

    private function toUtf8(string $value): string
    {
        if ($value === '' || mb_check_encoding($value, 'UTF-8')) {
            return $value;
        }

        return mb_convert_encoding($value, 'UTF-8', 'UTF-8,GB18030,GBK,Big5,ISO-8859-1');
    }

    /**
     * @param  array<string, string>  $row
     * @param  array<int, string>  $aliases
     */
    private function firstMatchedValue(array $row, array $aliases): string
    {
        foreach ($aliases as $alias) {
            $key = $this->normalizeImportHeader($alias);
            if (array_key_exists($key, $row)) {
                return (string) $row[$key];
            }
        }

        return '';
    }

    private function parseBooleanValue(string $value): bool
    {
        $normalized = strtolower(trim($value));
        if ($normalized === '') {
            return false;
        }

        return in_array($normalized, ['1', 'true', 'yes', 'y', 'on', '是', '真'], true);
    }

    private function buildUsersImportTemplateXlsx(string $xlsxPath): void
    {
        $zip = new \ZipArchive;
        if ($zip->open($xlsxPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            throw ValidationException::withMessages(['file' => ['无法生成 xlsx 模板文件。']]);
        }

        $contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
            .'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
            .'<Default Extension="xml" ContentType="application/xml"/>'
            .'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
            .'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
            .'</Types>';

        $rels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            .'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
            .'</Relationships>';

        $workbook = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
            .'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
            .'<sheets><sheet name="Users" sheetId="1" r:id="rId1"/></sheets></workbook>';

        $workbookRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            .'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
            .'</Relationships>';

        $rows = [
            ['nickname', 'email', 'phone', 'password', 'role_ids', 'force_password_change'],
            ['张三', 'zhangsan@example.com', '13800138000', 'Pass@123456', '1', '0'],
            ['李四', 'lisi@example.com', '13900139000', 'Pass@123456', '1,2', '1'],
        ];

        $sheetData = '';
        foreach ($rows as $rowIndex => $cells) {
            $r = $rowIndex + 1;
            $sheetData .= '<row r="'.$r.'">';
            foreach ($cells as $colIndex => $cellValue) {
                $column = chr(ord('A') + $colIndex).$r;
                $escapedValue = htmlspecialchars((string) $cellValue, ENT_XML1 | ENT_QUOTES, 'UTF-8');
                $sheetData .= '<c r="'.$column.'" t="inlineStr"><is><t>'.$escapedValue.'</t></is></c>';
            }
            $sheetData .= '</row>';
        }
        $sheet = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>'
            .$sheetData
            .'</sheetData></worksheet>';

        $zip->addFromString('[Content_Types].xml', $contentTypes);
        $zip->addFromString('_rels/.rels', $rels);
        $zip->addFromString('xl/workbook.xml', $workbook);
        $zip->addFromString('xl/_rels/workbook.xml.rels', $workbookRels);
        $zip->addFromString('xl/worksheets/sheet1.xml', $sheet);
        $zip->close();
    }

    private function syncServerResourcePermissionsByDirectPermissions(User $user, array $permissionIds): void
    {
        $resourcePermissions = Permission::query()
            ->select(['id', 'name', 'description'])
            ->whereIn('id', $permissionIds)
            ->where('guard_name', 'api')
            ->where('name', 'like', 'resource.servers.use.%')
            ->get();

        $grantedResourceIds = $resourcePermissions
            ->map(fn (Permission $permission): ?int => $this->resourceIdFromPermissionDescription((string) $permission->description))
            ->filter(fn (?int $resourceId): bool => $resourceId !== null)
            ->map(fn (int $resourceId): int => (int) $resourceId)
            ->values()
            ->all();

        if (count($grantedResourceIds) > 0) {
            $existingResourceIds = ServerResource::query()
                ->whereIn('id', $grantedResourceIds)
                ->whereNotNull('parent_id')
                ->pluck('id')
                ->map(fn (int $resourceId): int => (int) $resourceId)
                ->all();

            foreach ($existingResourceIds as $resourceId) {
                UserServerPermission::query()->updateOrCreate(
                    [
                        'user_id' => $user->id,
                        'server_resource_id' => $resourceId,
                    ],
                    [
                        'can_ssh' => true,
                        'can_sftp' => true,
                        'can_rdp' => true,
                    ]
                );
            }
        }

        $managedResourceIds = Permission::query()
            ->where('guard_name', 'api')
            ->where('name', 'like', 'resource.servers.use.%')
            ->where('description', 'like', '服务器资源访问权限（%资源ID:%')
            ->pluck('description')
            ->map(fn (string $description): ?int => $this->resourceIdFromPermissionDescription($description))
            ->filter(fn (?int $resourceId): bool => $resourceId !== null)
            ->map(fn (int $resourceId): int => $resourceId)
            ->values()
            ->all();

        if (count($managedResourceIds) > 0) {
            UserServerPermission::query()
                ->where('user_id', $user->id)
                ->whereIn('server_resource_id', $managedResourceIds)
                ->whereNotIn('server_resource_id', $grantedResourceIds)
                ->update([
                    'can_ssh' => false,
                    'can_sftp' => false,
                    'can_rdp' => false,
                ]);
        }
    }

    private function resourceIdFromPermissionDescription(string $description): ?int
    {
        if (! preg_match('/资源ID:\s*(\d+)/u', $description, $matches)) {
            return null;
        }

        return isset($matches[1]) ? (int) $matches[1] : null;
    }
}
