<?php

declare(strict_types=1);

namespace App\Http\Controllers\Market;

use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosSession;
use App\Models\Market\PosTransaction;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;

/**
 * Performance / load-testing endpoints.
 *
 * These bypass session auth so a client machine can hit them with curl.
 * Auth is via header `X-Perf-Token` matched against env `PERF_API_TOKEN`.
 * If `PERF_API_TOKEN` is unset the endpoints are disabled (403 for all).
 *
 * All endpoints return timing metrics (ms) so the caller can chart how the
 * box behaves under different batch sizes.
 */
class PerformanceController
{
    private const DEFAULT_LIMIT = 1000;

    private function authorize(Request $request)
    {
        $expected = (string) env('PERF_API_TOKEN', '');
        if ($expected === '') {
            return ResponseHelper::returnError('Performance API is disabled (PERF_API_TOKEN not set)', 403);
        }
        $provided = (string) ($request->header('X-Perf-Token') ?? $request->input('token', ''));
        if (!hash_equals($expected, $provided)) {
            return ResponseHelper::returnError('Invalid perf token', 401);
        }
        return null;
    }

    private function actingUser(): ?User
    {
        $hash = (string) env('PERF_ACTOR_HASH', '');
        if ($hash !== '') {
            $u = User::where('hashkey', $hash)->first();
            if ($u) return $u;
        }
        return User::where('acct_type', UserTypes::ULTIMATE->value)
            ->orderBy('id', 'asc')
            ->first();
    }

    private function clampCount($raw): int
    {
        $n = (int) $raw;
        if ($n < 1) $n = 1;
        if ($n > self::DEFAULT_LIMIT) $n = self::DEFAULT_LIMIT;
        return $n;
    }

    private function ms(float $start): float
    {
        return round((microtime(true) - $start) * 1000, 3);
    }

    public function ping(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;
        return Response::json([
            'success' => true,
            'ts' => now()->toIso8601String(),
            'php' => PHP_VERSION,
        ]);
    }

    /**
     * Core user seeder, returns ['hashes' => [...], 'ms' => float].
     */
    private function _seedUsers(User $actor, int $count, string $type, ?int $parentId, string $prefix): array
    {
        $parentId = $parentId ?? $actor->id;
        $hashed = Hash::make('Perf12345!');
        $start = microtime(true);
        $created = [];
        DB::beginTransaction();
        try {
            foreach (range(1, $count) as $i) {
                $suffix = Str::random(10);
                $u = User::create([
                    'username' => "{$prefix}_{$suffix}",
                    'name' => "Perf User {$i}",
                    'mobile_number' => '09' . str_pad((string) random_int(0, 999999999), 9, '0', STR_PAD_LEFT),
                    'password' => $hashed,
                    'acct_type' => $type,
                    'parentuid' => $parentId,
                    'active' => true,
                ]);
                $created[] = $u->hashkey;
            }
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            throw $e;
        }
        return ['hashes' => $created, 'ms' => $this->ms($start)];
    }

    private function _seedStores(User $actor, int $count, string $category, ?int $ownerId, string $prefix): array
    {
        $start = microtime(true);
        $created = [];
        DB::beginTransaction();
        try {
            foreach (range(1, $count) as $i) {
                $storeCode = StoreController::generateStoreCode($category);
                $s = Store::create([
                    'storecode' => $storeCode,
                    'name' => "{$prefix} " . Str::random(8),
                    'description' => "Synthetic store for perf testing #{$i}",
                    'address' => 'Perf Lane ' . random_int(1, 9999),
                    'category' => $category,
                    'subcategory' => '',
                    'owner_id' => $ownerId,
                    'created_by' => $actor->id,
                    'is_active' => true,
                    'status' => 'active',
                ]);
                $created[] = $s->hashkey;
            }
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            throw $e;
        }
        return ['hashes' => $created, 'ms' => $this->ms($start)];
    }

    private function _seedProducts(User $actor, int $count, ?Store $store, bool $attach, string $prefix): array
    {
        $start = microtime(true);
        $created = [];
        DB::beginTransaction();
        try {
            foreach (range(1, $count) as $i) {
                $price = random_int(10, 5000);
                $available = random_int(10, 500);
                $p = Product::create([
                    'name' => "{$prefix} " . Str::random(8),
                    'description' => "Synthetic product for perf testing #{$i}",
                    'price' => $price,
                    'unitname' => 'pc',
                    'available' => $available,
                    'category' => 'Perf',
                    'subcategory' => 'Synthetic',
                    'created_by' => $actor->id,
                    'is_active' => true,
                ]);
                if ($store && $attach) {
                    $store->products()->attach($p->id, [
                        'available' => $available,
                        'price' => $price,
                        'description' => $p->description,
                        'is_active' => true,
                    ]);
                }
                $created[] = $p->hashkey;
            }
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            throw $e;
        }
        return ['hashes' => $created, 'ms' => $this->ms($start)];
    }

    /**
     * POST /api/perf/seed/users
     * body: { count, type?, parent_hash?, prefix? }
     */
    public function seedUsers(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;
        $actor = $this->actingUser();
        if (!$actor) return ResponseHelper::returnError('No actor user available', 500);

        $count = $this->clampCount($request->input('count', 10));
        $type = (string) $request->input('type', UserTypes::USER->value);
        $prefix = (string) $request->input('prefix', 'perf');
        if (!UserTypes::tryFrom($type)) {
            return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
        }
        $parentId = null;
        if ($p = $request->input('parent_hash')) {
            $parent = User::where('hashkey', (string) $p)->first();
            if ($parent) $parentId = $parent->id;
        }
        try {
            $r = $this->_seedUsers($actor, $count, $type, $parentId, $prefix);
        } catch (\Throwable $e) {
            return ResponseHelper::returnError($e->getMessage());
        }
        return Response::json([
            'success' => true,
            'count' => count($r['hashes']),
            'total_ms' => $r['ms'],
            'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
            'sample' => array_slice($r['hashes'], 0, 5),
        ]);
    }

    /**
     * POST /api/perf/seed/stores
     * body: { count, owner_hash?, category?, prefix? }
     */
    public function seedStores(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;
        $actor = $this->actingUser();
        if (!$actor) return ResponseHelper::returnError('No actor user available', 500);

        $count = $this->clampCount($request->input('count', 10));
        $category = (string) $request->input('category', 'General');
        $prefix = (string) $request->input('prefix', 'PerfStore');
        $ownerId = null;
        if ($h = $request->input('owner_hash')) {
            $owner = User::where('hashkey', (string) $h)->first();
            if ($owner) $ownerId = $owner->id;
        }
        try {
            $r = $this->_seedStores($actor, $count, $category, $ownerId, $prefix);
        } catch (\Throwable $e) {
            return ResponseHelper::returnError($e->getMessage());
        }
        return Response::json([
            'success' => true,
            'count' => count($r['hashes']),
            'total_ms' => $r['ms'],
            'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
            'sample' => array_slice($r['hashes'], 0, 5),
        ]);
    }

    /**
     * POST /api/perf/seed/products
     * body: { count, target_store_hash?, prefix?, attach_to_store? }
     */
    public function seedProducts(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;
        $actor = $this->actingUser();
        if (!$actor) return ResponseHelper::returnError('No actor user available', 500);

        $count = $this->clampCount($request->input('count', 10));
        $prefix = (string) $request->input('prefix', 'PerfProduct');
        $attach = (bool) $request->input('attach_to_store', true);
        $store = null;
        if ($h = $request->input('target_store_hash')) {
            $store = Store::where('hashkey', (string) $h)->first();
            if (!$store) return ResponseHelper::returnError('Target store not found', 404);
        }
        try {
            $r = $this->_seedProducts($actor, $count, $store, $attach, $prefix);
        } catch (\Throwable $e) {
            return ResponseHelper::returnError($e->getMessage());
        }
        return Response::json([
            'success' => true,
            'count' => count($r['hashes']),
            'total_ms' => $r['ms'],
            'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
            'attached_to_store' => $store?->hashkey,
            'sample' => array_slice($r['hashes'], 0, 5),
        ]);
    }

    /**
     * POST /api/perf/seed/batch
     * body: { users?, stores?, products?, prefix?, type?, category? }
     * Runs all three seeders sequentially with per-phase timings.
     */
    public function seedBatch(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;
        $actor = $this->actingUser();
        if (!$actor) return ResponseHelper::returnError('No actor user available', 500);

        $prefix = (string) $request->input('prefix', 'perf');
        $type = (string) $request->input('type', UserTypes::USER->value);
        if (!UserTypes::tryFrom($type)) {
            return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
        }
        $category = (string) $request->input('category', 'General');

        $users = max(0, (int) $request->input('users', 0));
        $stores = max(0, (int) $request->input('stores', 0));
        $products = max(0, (int) $request->input('products', 0));

        $phases = [];
        $totalStart = microtime(true);

        try {
            if ($users > 0) {
                $r = $this->_seedUsers($actor, $this->clampCount($users), $type, null, $prefix);
                $phases['users'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
            }
            if ($stores > 0) {
                $r = $this->_seedStores($actor, $this->clampCount($stores), $category, null, $prefix . 'Store');
                $phases['stores'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
            }
            if ($products > 0) {
                $r = $this->_seedProducts($actor, $this->clampCount($products), null, false, $prefix . 'Product');
                $phases['products'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
            }
        } catch (\Throwable $e) {
            return ResponseHelper::returnError($e->getMessage());
        }

        return Response::json([
            'success' => true,
            'total_ms' => $this->ms($totalStart),
            'phases' => $phases,
        ]);
    }

    /**
     * POST /api/perf/pos/simulate
     * body: { store_hash, items?, cycles?, complete? }
     *
     * Runs end-to-end POS cycles entirely server-side: open session, add N
     * line items, optionally complete + archive. Returns per-phase timings.
     */
    public function simulatePos(Request $request)
    {
        $deny = $this->authorize($request);
        if ($deny) return $deny;

        $actor = $this->actingUser();
        if (!$actor) return ResponseHelper::returnError('No actor user available', 500);

        $storeHash = (string) $request->input('store_hash', '');
        if ($storeHash === '') return ResponseHelper::returnError('store_hash is required', 422);

        $store = Store::where('hashkey', $storeHash)->first();
        if (!$store) return ResponseHelper::returnError('Store not found', 404);

        $items = max(1, min(200, (int) $request->input('items', 5)));
        $cycles = max(1, min(100, (int) $request->input('cycles', 1)));
        $complete = (bool) $request->input('complete', true);

        // Pull a pool of products attached to this store; fall back to global
        // active products if the store has none yet.
        $productIds = DB::table('prd_str')
            ->where('store_id', $store->id)
            ->where('is_active', true)
            ->pluck('product_id')
            ->toArray();

        if (empty($productIds)) {
            $productIds = Product::where('is_active', true)
                ->limit(max(50, $items * 2))
                ->pluck('id')
                ->toArray();
        }

        if (empty($productIds)) {
            return ResponseHelper::returnError('No products available to simulate sales', 422);
        }

        $products = Product::whereIn('id', $productIds)->get(['id', 'hashkey', 'price'])->keyBy('id');

        $cycleResults = [];
        $totalStart = microtime(true);

        for ($c = 1; $c <= $cycles; $c++) {
            $cStart = microtime(true);

            // 1. Open session
            $t = microtime(true);
            $session = PosSession::create([
                'access_key' => Str::random(32),
                'store_id' => $store->id,
                'customer_name' => 'Perf Customer ' . $c,
                'status' => 'active',
                'created_by' => $actor->id,
            ]);
            $openMs = $this->ms($t);

            // 2. Add items (raw inserts for speed, mirrors PosController::addItem)
            $t = microtime(true);
            $now = now();
            $rows = [];
            $total = 0;
            for ($i = 0; $i < $items; $i++) {
                $pid = $productIds[array_rand($productIds)];
                $product = $products[$pid] ?? null;
                if (!$product) continue;
                $qty = random_int(1, 5);
                $price = (int) $product->price;
                $line = $price * $qty;
                $total += $line;
                $rows[] = [
                    'pos_session_id' => $session->id,
                    'product_id' => $pid,
                    'quantity' => $qty,
                    'price_at_sale' => $price,
                    'total_price' => $line,
                    'hashkey' => Str::uuid()->toString() . Str::random(100),
                    'created_at' => $now,
                    'updated_at' => $now,
                    'created_by' => $actor->id,
                ];
            }
            if (!empty($rows)) {
                DB::table('pos_transactions')->insert($rows);
            }
            $addMs = $this->ms($t);

            // 3. Optionally complete the session
            $completeMs = null;
            if ($complete) {
                $t = microtime(true);
                DB::table('pos_sessions')->where('id', $session->id)->update([
                    'total_amount' => $total,
                    'received_amount' => $total,
                    'change_amount' => 0,
                    'status' => 'completed',
                    'payment_method' => 'cash',
                    'updated_at' => $now,
                    'updated_by' => $actor->id,
                ]);
                $completeMs = $this->ms($t);
            }

            $cycleResults[] = [
                'session_hash' => $session->hashkey,
                'items' => count($rows),
                'total' => $total,
                'open_ms' => $openMs,
                'add_items_ms' => $addMs,
                'complete_ms' => $completeMs,
                'cycle_ms' => $this->ms($cStart),
            ];
        }

        $totalMs = $this->ms($totalStart);
        $cycleMsValues = array_column($cycleResults, 'cycle_ms');
        $avg = $cycleMsValues ? array_sum($cycleMsValues) / count($cycleMsValues) : 0.0;

        return Response::json([
            'success' => true,
            'store_hash' => $store->hashkey,
            'cycles' => $cycles,
            'items_per_cycle' => $items,
            'total_ms' => $totalMs,
            'avg_cycle_ms' => round($avg, 3),
            'min_cycle_ms' => $cycleMsValues ? min($cycleMsValues) : 0,
            'max_cycle_ms' => $cycleMsValues ? max($cycleMsValues) : 0,
            'detail' => $cycleResults,
        ]);
    }
}
