<?php

namespace Modules\PortalApi\Http\Controllers\Api;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Modules\PortalApi\Entities\PortalBrand;
use Modules\PortalApi\Entities\PortalMagicToken;
use Modules\PortalApi\Entities\PortalOtpChallenge;
use Modules\PortalApi\Entities\PortalOwner;
use Modules\PortalApi\Entities\PortalOwnerToken;
use Modules\PortalApi\Mail\PortalMagicLink;
use Modules\PortalApi\Services\Sms\PortalSmsGateway;
use Modules\PortalApi\Http\Controllers\Api\DevAuthController;

class AuthController extends Controller
{
    /* ─── Email + password ─────────────────────────────────────── */

    public function password(Request $request): JsonResponse
    {
        $data = $request->validate([
            'email'       => 'required|email|max:191',
            'password'    => 'required|string|min:6|max:191',
            'clinic_slug' => 'nullable|string|max:64',
        ]);

        $brand = $this->resolveBrand($request, $data['clinic_slug'] ?? null);
        if ($brand === false) return $this->unknownClinic();

        $owner = $this->ownersForBrand($brand)
            ->where('email', $data['email'])
            ->where('is_active', true)
            ->first();

        if (! $owner || ! $owner->password || ! Hash::check($data['password'], $owner->password)) {
            // Same generic message whether the email exists or not.
            return response()->json(['message' => 'Invalid email or password.'], 422);
        }

        return $this->issueAndReturn($owner, $request);
    }

    /* ─── Phone OTP ────────────────────────────────────────────── */

    public function otpRequest(Request $request, PortalSmsGateway $sms): JsonResponse
    {
        $data = $request->validate([
            'phone'       => 'required|string|min:8|max:32',
            'clinic_slug' => 'nullable|string|max:64',
        ]);
        $phone = $this->normalisePhone($data['phone']);

        $brand = $this->resolveBrand($request, $data['clinic_slug'] ?? null);
        if ($brand === false) return $this->unknownClinic();

        $owner = $this->ownersForBrand($brand)
            ->where('phone', $phone)
            ->where('is_active', true)
            ->first();

        // Self-registration: if no owner with this phone exists, do NOT auto-create
        // for v1 — clinic admins provision. Return a generic message so we don't
        // leak which numbers are registered.
        if (! $owner) {
            // Pretend it worked. Throttling + rate-limit middleware should be
            // added in front of this endpoint in v1.1.
            return response()->json([
                'challenge' => (string) \Illuminate\Support\Str::uuid(),
                'sent'      => false,
            ]);
        }

        $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        $challenge = PortalOtpChallenge::open(
            phone: $phone,
            ownerId: $owner->id,
            codeHash: Hash::make($code),
        );

        $sms->sendOtp($phone, $code, PortalOtpChallenge::TTL_MINUTES);

        $response = [
            'challenge' => $challenge->id,
            'sent'      => true,
        ];
        // DEV CONVENIENCE: when PORTAL_DEV_LOGIN (or APP_DEBUG) is on,
        // return the actual code so the operator / test-sign-in panel can
        // skip SMS delivery and complete the round-trip locally.
        if (DevAuthController::enabled()) {
            $response['dev_code'] = $code;
        }
        return response()->json($response);
    }

    public function otpVerify(Request $request): JsonResponse
    {
        $data = $request->validate([
            'challenge' => 'required|uuid',
            'code'      => 'required|string|size:6',
        ]);

        $row = PortalOtpChallenge::find($data['challenge']);
        if (! $row || ! $row->isFresh()) {
            return response()->json(['message' => 'This code has expired. Please request a new one.'], 422);
        }

        if (! Hash::check($data['code'], $row->code_hash)) {
            $row->increment('attempts');
            return response()->json(['message' => 'Incorrect code.'], 422);
        }

        $row->forceFill(['used_at' => now()])->save();

        $owner = $row->owner_id ? PortalOwner::find($row->owner_id) : null;
        if (! $owner || ! $owner->is_active) {
            return response()->json(['message' => 'This account is no longer active.'], 422);
        }

        return $this->issueAndReturn($owner, $request);
    }

    /* ─── Email magic link ─────────────────────────────────────── */

    public function magicRequest(Request $request): JsonResponse
    {
        $data = $request->validate([
            'email'       => 'required|email|max:191',
            'clinic_slug' => 'nullable|string|max:64',
        ]);

        $brand = $this->resolveBrand($request, $data['clinic_slug'] ?? null);
        if ($brand === false) return $this->unknownClinic();

        $owner = $this->ownersForBrand($brand)
            ->where('email', $data['email'])
            ->where('is_active', true)
            ->first();

        // Don't leak whether the address is registered.
        if (! $owner) {
            return response()->json(['sent' => true]);
        }

        $token = PortalMagicToken::issue($owner, $data['email'], $request->userAgent(), $request->ip());
        $magicUrl = url('/portal-auth/magic/' . $token);

        // Dev convenience: always log the URL so SMTP-less machines can still
        // exercise the flow. Strip this in prod by setting APP_DEBUG=false.
        if (config('app.debug')) {
            Log::info('[PortalAuth] Magic link generated', [
                'email' => $data['email'],
                'url'   => $magicUrl,
            ]);
        }

        try {
            Mail::to($data['email'])->send(new PortalMagicLink(
                ownerName:  $owner->name,
                clinicName: config('app.name', 'Pet Portal'),
                magicUrl:   $magicUrl,
                minutes:    PortalMagicToken::TTL_MINUTES,
            ));
        } catch (\Throwable $e) {
            Log::warning('[PortalAuth] Magic link mail failed', [
                'error' => $e->getMessage(),
                'url'   => $magicUrl,
            ]);
        }

        $response = ['sent' => true];
        // DEV CONVENIENCE: surface the magic URL so the test-sign-in
        // panel can complete the round-trip without SMTP.
        if (DevAuthController::enabled()) {
            $response['dev_magic_url'] = $magicUrl;
        }
        return response()->json($response);
    }

    /**
     * Magic link click. The user lands here from their email.
     * We verify the token and bounce to the PWA at /app/?magic=<bearer>.
     * The PWA's main.ts reads ?magic= on boot, calls setToken(), strips the
     * query string, and navigates to /home.
     */
    public function magicClick(Request $request, string $token)
    {
        $row = PortalMagicToken::consume($token);
        if (! $row) {
            return response()->view('portalApi::magic_invalid', [], 410);
        }

        $owner = PortalOwner::find($row->owner_id);
        if (! $owner || ! $owner->is_active) {
            return response()->view('portalApi::magic_invalid', [], 410);
        }

        $bearer = PortalOwnerToken::issue($owner, $request->userAgent(), $request->ip());
        $owner->forceFill(['last_login_at' => now()])->save();

        // In production the PWA is served by Laravel at /app/ (same origin).
        // In dev Vite serves it at e.g. http://localhost:5175/app/ — set
        // PORTAL_PWA_URL in .env to that value to bounce magic-link clicks
        // back into the running dev server.
        $pwaUrl = rtrim(env('PORTAL_PWA_URL', url('/app/')), '/');
        return redirect()->to($pwaUrl . '/?magic=' . urlencode($bearer));
    }

    /* ─── Session ─────────────────────────────────────────────── */

    public function logout(Request $request): JsonResponse
    {
        $token = $request->bearerToken();
        if ($token) {
            PortalOwnerToken::where('token_hash', hash('sha256', $token))->delete();
        }
        return response()->json(['ok' => true]);
    }

    /* ─── Helpers ─────────────────────────────────────────────── */

    private function issueAndReturn(PortalOwner $owner, Request $request): JsonResponse
    {
        $bearer = PortalOwnerToken::issue($owner, $request->userAgent(), $request->ip());
        $owner->forceFill(['last_login_at' => now()])->save();

        return response()->json([
            'token' => $bearer,
            'me'    => [
                'id'    => $owner->id,
                'name'  => $owner->name,
                'phone' => $owner->phone,
                'email' => $owner->email,
            ],
        ]);
    }

    /** Strip whitespace + leading 00 → + (Egyptian number style). */
    private function normalisePhone(string $phone): string
    {
        $p = preg_replace('/\s+/', '', $phone);
        if (str_starts_with($p, '00')) {
            $p = '+' . substr($p, 2);
        }
        return $p;
    }

    /**
     * Pick the clinic brand for this login attempt.
     *
     * Returns:
     *   - PortalBrand    when resolved
     *   - false          when the caller explicitly named a slug that
     *                    doesn't exist → caller should reject
     *   - null           when no slug given and no brand registered yet
     *                    (fresh install) → caller falls back to unscoped
     */
    private function resolveBrand(Request $request, ?string $slug)
    {
        if ($slug !== null && $slug !== '') {
            $b = PortalBrand::forSlug($slug);
            return $b ?: false;
        }
        return PortalBrand::forRequest($request);
    }

    /**
     * Owner lookup query scoped to the brand's business.
     *   PortalBrand  → scope to that business
     *   false        → an unknown slug was passed; caller already rejected
     *   null         → no brand at all (fresh install) → unscoped so the
     *                  demo seed still works on a blank database
     */
    private function ownersForBrand($brand)
    {
        $q = PortalOwner::query();
        if ($brand instanceof PortalBrand) {
            $q->where('business_id', $brand->business_id);
        }
        return $q;
    }

    /** Short-circuit response for an unknown clinic slug. */
    private function unknownClinic(): JsonResponse
    {
        return response()->json([
            'message' => 'This clinic portal could not be found.',
        ], 404);
    }
}
