<?php

namespace App\Services;

use App\Core\IpAllocator\AllocatorManager;
use App\Models\IpAddress;
use App\Models\IpPool;
use App\Models\IpPoolImport;
use IPLib\Address\IPv4;
use IPLib\Range\Subnet;
use Illuminate\Support\Facades\DB;

class IpAllocationService
{
    public function __construct(protected AllocatorManager $manager)
    {
    }

    public function importCidr(IpPool $pool, string $cidr, ?int $adminId = null): IpPoolImport
    {
        $range = Subnet::fromString($cidr);
        if (! $range || ! ($range->getStartAddress() instanceof IPv4)) {
            throw new \InvalidArgumentException("Invalid IPv4 CIDR: {$cidr}");
        }

        $reserved = collect([
            optional($pool->server)->ip_address,
            $pool->gateway,
            $range->getStartAddress()->toString(),
            $range->getEndAddress()->toString(),
        ])->filter()->unique()->values()->all();

        $rows = [];
        $free = 0;
        $reservedCount = 0;
        $now = now();

        $startLong = ip2long($range->getStartAddress()->toString());
        $endLong = ip2long($range->getEndAddress()->toString());

        for ($n = $startLong; $n <= $endLong; $n++) {
            $addr = long2ip($n);
            $isReserved = in_array($addr, $reserved, true);
            $rows[] = [
                'pool_id' => $pool->id,
                'address' => $addr,
                'address_long' => $n,
                'status' => $isReserved ? 'reserved' : 'free',
                'notes' => $isReserved ? $this->reservedReason($addr, $pool, $range) : null,
                'created_at' => $now,
                'updated_at' => $now,
            ];
            $isReserved ? $reservedCount++ : $free++;
        }

        DB::transaction(function () use ($rows) {
            foreach (array_chunk($rows, 1000) as $chunk) {
                IpAddress::insert($chunk);
            }
        });

        return IpPoolImport::create([
            'pool_id' => $pool->id,
            'cidr' => $cidr,
            'source' => 'cidr',
            'total_count' => $free + $reservedCount,
            'free_count' => $free,
            'reserved_count' => $reservedCount,
            'imported_by_admin_id' => $adminId,
        ]);
    }

    public function importRange(IpPool $pool, string $start, string $end, ?int $adminId = null): IpPoolImport
    {
        $startLong = ip2long($start);
        $endLong = ip2long($end);
        if ($startLong === false || $endLong === false || $startLong > $endLong) {
            throw new \InvalidArgumentException("Invalid IPv4 range: {$start} - {$end}");
        }

        $rows = [];
        $now = now();
        for ($n = $startLong; $n <= $endLong; $n++) {
            $addr = long2ip($n);
            $rows[] = [
                'pool_id' => $pool->id,
                'address' => $addr,
                'address_long' => $n,
                'status' => 'free',
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        DB::transaction(function () use ($rows) {
            foreach (array_chunk($rows, 1000) as $chunk) {
                IpAddress::insert($chunk);
            }
        });

        return IpPoolImport::create([
            'pool_id' => $pool->id,
            'source' => 'range',
            'total_count' => count($rows),
            'free_count' => count($rows),
            'reserved_count' => 0,
            'imported_by_admin_id' => $adminId,
        ]);
    }

    public function importManual(IpPool $pool, array $addresses, ?int $adminId = null): IpPoolImport
    {
        $rows = [];
        $now = now();
        foreach ($addresses as $addr) {
            if (! filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                throw new \InvalidArgumentException("Invalid IPv4: {$addr}");
            }
            $rows[] = [
                'pool_id' => $pool->id,
                'address' => $addr,
                'address_long' => ip2long($addr),
                'status' => 'free',
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        DB::transaction(function () use ($rows) {
            IpAddress::insert($rows);
        });

        return IpPoolImport::create([
            'pool_id' => $pool->id,
            'source' => 'manual',
            'total_count' => count($rows),
            'free_count' => count($rows),
            'reserved_count' => 0,
            'imported_by_admin_id' => $adminId,
        ]);
    }

    protected function reservedReason(string $addr, IpPool $pool, Subnet $range): string
    {
        if ($addr === $range->getStartAddress()->toString()) {
            return 'network';
        }
        if ($addr === $range->getEndAddress()->toString()) {
            return 'broadcast';
        }
        if ($pool->gateway && $addr === $pool->gateway) {
            return 'gateway';
        }
        if (optional($pool->server)->ip_address === $addr) {
            return 'server-self';
        }
        return 'reserved';
    }

    public function allocateForProduct(\App\Models\ClientProduct $cp, ?int $poolOverrideId = null, string $via = 'requires_ip'): IpAddress
    {
        $pool = $this->resolvePool($cp, $poolOverrideId);
        $allocator = $this->manager->get($pool->allocator);

        return DB::transaction(function () use ($pool, $allocator, $cp, $via) {
            $ip = $allocator->allocate($pool);
            $ip->update([
                'client_product_id' => $cp->id,
                'allocated_via' => $via,
            ]);
            $cp->update([
                'ip_address_id' => $ip->id,
                'dedicated_ip' => $ip->address,
            ]);
            return $ip->fresh();
        });
    }

    public function release(IpAddress $ip, ?int $quarantineDays = null): void
    {
        $days = $quarantineDays ?? $ip->pool->quarantine_days ?? 7;
        DB::transaction(function () use ($ip, $days) {
            if ($ip->client_product_id) {
                \App\Models\ClientProduct::where('id', $ip->client_product_id)
                    ->update(['ip_address_id' => null]);
            }
            $ip->update([
                'status' => 'quarantined',
                'client_product_id' => null,
                'assigned_at' => null,
                'released_at' => now()->addDays($days),
            ]);
        });
    }

    public function forceRelease(IpAddress $ip): void
    {
        DB::transaction(function () use ($ip) {
            if ($ip->client_product_id) {
                \App\Models\ClientProduct::where('id', $ip->client_product_id)
                    ->update(['ip_address_id' => null]);
            }
            $ip->update([
                'status' => 'free',
                'client_product_id' => null,
                'assigned_at' => null,
                'released_at' => null,
            ]);
        });
    }

    public function reserve(IpAddress $ip, string $reason): void
    {
        $ip->update([
            'status' => 'reserved',
            'notes' => trim(($ip->notes ? $ip->notes . "\n" : '') . $reason),
        ]);
    }

    public function unreserve(IpAddress $ip): void
    {
        if ($ip->status !== 'reserved') {
            return;
        }
        $ip->update(['status' => 'free']);
    }

    public function importLegacyToPool(\App\Models\ClientProduct $cp, IpPool $pool, ?int $adminId = null): IpAddress
    {
        if (! $cp->dedicated_ip) {
            throw new \InvalidArgumentException("ClientProduct {$cp->id} has no legacy dedicated_ip");
        }
        if (! filter_var($cp->dedicated_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            throw new \InvalidArgumentException("ClientProduct {$cp->id} dedicated_ip is invalid IPv4: {$cp->dedicated_ip}");
        }

        return DB::transaction(function () use ($cp, $pool, $adminId) {
            $existing = IpAddress::where('pool_id', $pool->id)->where('address', $cp->dedicated_ip)->first();

            if ($existing) {
                if ($existing->status === 'used' && $existing->client_product_id !== $cp->id) {
                    throw new \RuntimeException("IP {$cp->dedicated_ip} is already assigned to another client_product");
                }
                $existing->update([
                    'status' => 'used',
                    'client_product_id' => $cp->id,
                    'allocated_via' => 'manual_admin',
                    'assigned_at' => $existing->assigned_at ?? now(),
                ]);
                $ip = $existing;
            } else {
                $ip = IpAddress::create([
                    'pool_id' => $pool->id,
                    'address' => $cp->dedicated_ip,
                    'address_long' => ip2long($cp->dedicated_ip),
                    'status' => 'used',
                    'client_product_id' => $cp->id,
                    'allocated_via' => 'manual_admin',
                    'assigned_at' => now(),
                    'notes' => 'imported from legacy dedicated_ip',
                ]);
            }

            IpPoolImport::create([
                'pool_id' => $pool->id,
                'source' => 'manual_legacy',
                'total_count' => 1,
                'free_count' => 0,
                'reserved_count' => 0,
                'imported_by_admin_id' => $adminId,
            ]);

            $cp->update(['ip_address_id' => $ip->id]);

            return $ip;
        });
    }

    protected function resolvePool(\App\Models\ClientProduct $cp, ?int $poolOverrideId): IpPool
    {
        if ($poolOverrideId) {
            $pool = IpPool::find($poolOverrideId);
            if (! $pool) {
                throw new \InvalidArgumentException("IP pool {$poolOverrideId} not found");
            }
            return $pool;
        }

        if (! $cp->server_id) {
            throw new \App\Core\IpAllocator\Exceptions\NoPoolForServerException("ClientProduct {$cp->id} has no server_id; cannot resolve pool");
        }

        $pool = IpPool::active()->forServer($cp->server_id)->where('is_default', true)->first()
            ?? IpPool::active()->forServer($cp->server_id)->orderBy('id')->first();

        if (! $pool) {
            throw \App\Core\IpAllocator\Exceptions\NoPoolForServerException::for($cp->server_id);
        }

        return $pool;
    }
}
