<?php

namespace App\Jobs;

use App\Models\AdminBatchJob;
use App\Models\Role;
use App\Models\User;
use App\Services\IdentityLimitService;
use App\Services\TreeService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class ImportMembersJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private readonly int $batchId)
    {
    }

    public function handle(IdentityLimitService $identityLimitService, TreeService $treeService): void
    {
        $batch = AdminBatchJob::query()->find($this->batchId);
        if (! $batch) {
            return;
        }

        $batch->status = 'processing';
        $batch->started_at = now();
        $batch->save();

        $path = Storage::disk('local')->path($batch->file_path);
        if (! file_exists($path)) {
            $batch->status = 'failed';
            $batch->finished_at = now();
            $batch->save();
            return;
        }

        $handle = fopen($path, 'r');
        if (! $handle) {
            $batch->status = 'failed';
            $batch->finished_at = now();
            $batch->save();
            return;
        }

        $headers = fgetcsv($handle);
        if (! $headers) {
            fclose($handle);
            $batch->status = 'failed';
            $batch->finished_at = now();
            $batch->save();
            return;
        }

        $headers = array_map(fn ($h) => strtolower(trim((string) $h)), $headers);
        $rowNumber = 1;
        $errors = [];
        $processed = 0;
        $failed = 0;
        $createdAny = false;
        $ignoreLimits = (bool) data_get($batch->meta, 'ignore_limits', false);

        while (($row = fgetcsv($handle)) !== false) {
            $rowNumber++;
            $data = array_combine($headers, $row) ?: [];
            $name = trim((string) ($data['name'] ?? ''));
            $mobile = trim((string) ($data['mobile'] ?? ''));
            $email = trim((string) ($data['email'] ?? ''));
            $sponsorCode = trim((string) ($data['sponsor_code'] ?? $data['sponsor'] ?? ''));
            $placementLeg = strtolower(trim((string) ($data['placement_leg'] ?? 'auto')));
            $packageAmount = (float) ($data['package_amount'] ?? 0);
            $password = (string) ($data['password'] ?? Str::random(10));

            if ($name === '' || $mobile === '' || $sponsorCode === '') {
                $errors[] = ['row' => $rowNumber, 'error' => 'Missing required fields'];
                $failed++;
                continue;
            }

            if (! in_array($placementLeg, ['left', 'right', 'auto'], true)) {
                $placementLeg = 'auto';
            }

            try {
                if (! $ignoreLimits) {
                    $identityLimitService->assertCanCreate([
                        'mobile' => $mobile,
                        'email' => $email ?: null,
                    ]);
                }

                $sponsor = User::query()
                    ->where('referral_code', $sponsorCode)
                    ->where('role', 'user')
                    ->first();
                if (! $sponsor) {
                    throw new \RuntimeException('Sponsor not found');
                }

                $resolvedLeg = $placementLeg === 'auto' ? $this->resolvePlacementLeg($sponsor->id) : $placementLeg;
                $placementTaken = User::query()
                    ->where('placement_parent_id', $sponsor->id)
                    ->where('placement_leg', $resolvedLeg)
                    ->exists();
                if ($placementTaken) {
                    throw new \RuntimeException('Placement leg already occupied');
                }

                DB::transaction(function () use ($name, $email, $mobile, $password, $sponsor, $resolvedLeg, $packageAmount, &$createdAny): void {
                    $user = User::query()->create([
                        'member_code' => $this->generateUniqueCode('AKR', 'member_code'),
                        'referral_code' => $this->generateUniqueCode('REF', 'referral_code'),
                        'name' => $name,
                        'email' => $email !== '' ? $email : null,
                        'mobile' => $mobile,
                        'password' => Hash::make($password),
                        'role' => 'user',
                        'status' => 'pending',
                        'sponsor_id' => $sponsor->id,
                        'placement_parent_id' => $sponsor->id,
                        'placement_leg' => $resolvedLeg,
                        'package_amount' => $packageAmount,
                    ]);

                    DB::table('wallet_balances')->insert(['user_id' => $user->id]);
                    DB::table('user_binary_carry')->insert(['user_id' => $user->id]);

                    $distributorRole = Role::query()->where('code', 'distributor')->first();
                    if ($distributorRole) {
                        $user->roles()->syncWithoutDetaching([$distributorRole->id]);
                    }

                    $createdAny = true;
                });

                $processed++;
            } catch (\Throwable $exception) {
                $errors[] = ['row' => $rowNumber, 'error' => $exception->getMessage()];
                $failed++;
            }
        }

        fclose($handle);

        $errorPath = null;
        if ($errors !== []) {
            $errorPath = 'imports/members/errors-'.$batch->id.'.json';
            Storage::disk('local')->put($errorPath, json_encode($errors, JSON_PRETTY_PRINT));
        }

        $batch->status = $failed > 0 ? 'completed_with_errors' : 'completed';
        $batch->total_rows = $processed + $failed;
        $batch->processed_rows = $processed;
        $batch->failed_rows = $failed;
        $batch->error_report_path = $errorPath;
        $batch->finished_at = now();
        $batch->save();

        if ($createdAny) {
            $treeService->rebuildClosure();
        }
    }

    private function generateUniqueCode(string $prefix, string $column): string
    {
        do {
            $candidate = $prefix.strtoupper(bin2hex(random_bytes(4)));
        } while (User::query()->where($column, $candidate)->exists());

        return $candidate;
    }

    private function resolvePlacementLeg(int $parentId): string
    {
        $leftOccupied = User::query()
            ->where('placement_parent_id', $parentId)
            ->where('placement_leg', 'left')
            ->exists();
        $rightOccupied = User::query()
            ->where('placement_parent_id', $parentId)
            ->where('placement_leg', 'right')
            ->exists();

        if (! $leftOccupied) {
            return 'left';
        }

        if (! $rightOccupied) {
            return 'right';
        }

        $left = (float) DB::table('binary_ledger')
            ->where('beneficiary_user_id', $parentId)
            ->where('leg', 'left')
            ->selectRaw("COALESCE(SUM(CASE WHEN direction = 'credit' THEN units ELSE -units END), 0) as total")
            ->value('total');

        $right = (float) DB::table('binary_ledger')
            ->where('beneficiary_user_id', $parentId)
            ->where('leg', 'right')
            ->selectRaw("COALESCE(SUM(CASE WHEN direction = 'credit' THEN units ELSE -units END), 0) as total")
            ->value('total');

        return $left <= $right ? 'left' : 'right';
    }
}
