<?php

namespace App\Http\Controllers\Auth;

// Laravel Specifics
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

// Creatia Specifics
use App\Helpers\Security;
use App\Http\Controllers\BaseController;
use App\Models\Alert;
use App\Models\AlertDisplay;
use App\Models\User;
use App\Models\Asns;
use App\Models\EventLease;
use App\Models\FailedLogin;
use App\Models\Organisation;
use App\Models\SecurityEvent;
use Illuminate\Support\Facades\Notification;
use App\Notifications\SlackNotification;
use Illuminate\Support\Facades\Hash;
use PragmaRX\Google2FA\Google2FA;

class AuthController extends BaseController
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login', 'reset']]);
    }

    public function add_failed_login($ip, $user_email, $user_agent)
    {
        $user = User::where('email', '=', $user_email)->first() ?? null;

        if($user != null) {
            FailedLogin::create([
                'ip'        => $ip,
                'user_agent'=> $user_agent,
                'user_id'   => $user->id
            ]);
        }
    }

    public function check_excessive_account_attempts($user_email)
    {
        $user = User::where('email', '=', $user_email)->first() ?? null;
        $count = 0;

        if($user != null) {
            if($user->id) {
                $count = FailedLogin::where( [
                    ['user_id', '=', $user->id],
                    ['created_at', '>', Carbon::now()->subMinutes(config('security.account_lockout_time'))] 
                ] )->count();    
            }
        }

        return $count >= config('security.account_login_attempts');
    }

    // Hardened login handling
    public function login(Request $_request)
    {
        $real_ip = Security::real_ip();
        $response = null;

        try {
            // Only perform our general login procedure if there hasn't been an excessive amount of attempts against a given account.
            if(!$this->check_excessive_account_attempts($_request->input('email'))) {
                $executed = RateLimiter::attempt(
                    "login-attempt:$real_ip",
                    $perMinute = config('security.client_login_attempts'),
                    function() use($_request, &$response, $real_ip) {

                        $valid_params = !Validator::make($_request->all(), [
                            'email'    => 'required',
                            'password' => 'required',
                        ])->fails();

                        if ($valid_params) {
                            // Resolve the user case-insensitively (your old fallback, now the primary path)
                            $user = User::whereRaw('lower(email) = ?', [strtolower($_request->input('email'))])->first();

                            if ($user && Hash::check($_request->input('password'), $user->password)) {

                                $mfa_passed = true;

                                if($user->mfaEnabled()) {
                                    if(Security::mfa_bypassed()) {
                                        SecurityEvent::create([
                                            'user_id'       => $user->id,
                                            'ip'            => $real_ip,
                                            'user_agent'    => $_request->userAgent(),
                                            'route'         => $_request->path(),
                                            'payload'       => json_encode([]),
                                            'description'   => 'MFA bypassed - internal network',
                                        ]);
                                    }
                                    else {
                                        $mfa_passed = false;

                                        $code = (string) $_request->input('code', '');
                                        $slice = false;

                                        if(preg_match('/^\d{6}$/', $code)) {
                                            $slice = (new Google2FA)->verifyKeyNewer(
                                                $user->totp_secret,
                                                $code,
                                                $user->totp_last_slice ?? 0,
                                                (int) config('security.mfa_totp_window')
                                            );
                                        }

                                        if($slice !== false) {
                                            // Remember the accepted timeslice so the same code cannot be replayed within its window.
                                            $user->totp_last_slice = $slice;
                                            $user->save();

                                            $mfa_passed = true;
                                        }
                                        else {
                                            // An absent code is the first leg of the two-step flow, not a failed attempt -
                                            // Only a supplied-but-wrong code counts towards the account lockout.
                                            if($code !== '') {
                                                $this->add_failed_login($real_ip, $_request->input('email'), $_request->userAgent());

                                                SecurityEvent::create([
                                                    'user_id'       => $user->id,
                                                    'ip'            => $real_ip,
                                                    'user_agent'    => $_request->userAgent(),
                                                    'route'         => $_request->path(),
                                                    'payload'       => json_encode([]),
                                                    'description'   => 'Failed MFA code',
                                                ]);
                                            }

                                            $response = response()->json(['detail' => 'MFA code required'], 401);
                                        }
                                    }
                                }

                                if($mfa_passed) {
                                    $expiresAt = now()->addMinutes((int) config('sanctum.expiration'));
                                    $token     = $user->createToken('api', ['*'], $expiresAt)->plainTextToken;

                                    $response = response()->json([
                                        'id'                => $user->id,
                                        'email'             => $user->email,
                                        'name'              => $user->name,
                                        'organisation_id'   => $user->organisation_id,
                                        'organisation_name' => Organisation::find($user->organisation_id)?->name,
                                        'access_token'      => $token,
                                        'expiration'        => $expiresAt->timestamp,
                                        'permissions'       => $user->getPermissions(),
                                        'mfa'               => $user->mfaEnabled(),
                                        'has_asns'          => Asns::where('organisation_id', '=', $user->organisation_id)->exists(),
                                        'alerts'            => $this->helper_retrieve_alerts($user->id),
                                    ], 200);
                                }
                            }
                            else {
                                $response = response()->json(['detail' => 'Invalid credentials'], 401);
                                $this->add_failed_login($real_ip, $_request->input('email'), $_request->userAgent());
                            }
                        }
                        else {
                            $response = response()->json(['detail' => 'Invalid credentials'], 401);
                        }
                    }
                );
        
                // If there has been too many login attempts, redirect the user to the login screen...
                if (!$executed) {
                    // Get the seconds until the next login attempt
                    $seconds = RateLimiter::availableIn("login-attempt:$real_ip");
        
                    // Login attempts succeeded redirect object.
                    $response = response()->json(['detail' => "Attempts exceeded. Try again in $seconds seconds"], 429);
                }
            }
            else {
                $response = response()->json(['detail' => "Attempts exceeded. Try again later"], 429);
            }
        }
        catch(\Throwable $e) {
            $response = response()->json(['detail' => 'Server error'], 500);
        }

        return $response;
    }

    public function reset(Request $_request)
    {
        try {
            if($user = User::where('email', '=', $_request->input('email'))->first()) {
                $user->sendResetEmail();
            }
        }
        catch(\Throwable $e) {
            return response()->json(['detail' => 'Server error'], 500);
        }

        // Return a 200 on success so that the end client cannot determine anything about the emails known.
        return response()->json([], 200);
    }

    public function refresh(Request $_request)
    {
        try {
            $user = $_request->user();

            optional($user->currentAccessToken())->delete(); // Kill the old token

            $expire_at = now()->addMinutes((int) config('sanctum.expiration'));
            $token     = $user->createToken('api', ['*'], $expire_at)->plainTextToken;

            User::where('id', $user->id)->update(['refreshed_at' => Carbon::now()]);

            return response()->json([
                'access_token' => $token,
                'expiration'   => $expire_at->timestamp,
            ], 200);
        } catch (\Throwable $ex) {
            return response()->json([], 400);
        }
    }

    public function lease(Request $_request)
    {
        try {

            if(Validator::make($_request->input(), [
                'resource' => ['required', Rule::in([ 'weathermap', 'droppedTraffic', 'mercury' ])]
            ])->fails()) {
                return response()->json([], 400);
            }

            $relevant_lease = EventLease::where([
                [ 'resource', '=', $_request->input('resource') ],
                [ 'user_id', '=',  Auth::user()->id ]
            ])->count();

            if(!$relevant_lease) {
                EventLease::create([
                    'resource'      => $_request->input('resource'),
                    'user_id'       => Auth::user()->id,
                    'leased_at'     => Carbon::now()
                ]);
            }
            else {
                EventLease::where([
                    [ 'resource', '=', $_request->input('resource') ],
                    [ 'user_id', '=',  Auth::user()->id ]
                ])->update([
                    'leased_at' => Carbon::now()
                ]);
            }

            return response()->json([], 200);

        } catch (\Throwable $ex) {
            \Log::debug($ex);
            return response()->json([], 500);
        }
    }

    public function me()
    {
        $user = Auth::user();

        User::where('id', '=', $user->id)->update([
            'refreshed_at' => Carbon::now()
        ]);

        return response()->json([
            'id' => $user->id,
            'email' => $user->email,
            'name' => $user->name,
            'permissions' => $user->getPermissions(),
            'mfa' => $user->mfaEnabled(),
            'organisation_id' => $user->organisation_id,
            'organisation_name' => Organisation::find($user->organisation_id)->name,
            'has_asns' => Asns::where('organisation_id', '=', $user->organisation_id)->count() ? true : false,
            'alerts' => $this->helper_retrieve_alerts($user->id)
        ]);
    }

    public function logout(Request $_request)
    {

        SecurityEvent::create([
            'user_id'       => Auth::user()->id,
            'ip'            => Security::real_ip(),
            'user_agent'    => $_request->userAgent(),
            'route'         => $_request->path(),
            'payload'       => json_encode([]),
            'description'   => 'Successful logout',
        ]);

        optional($_request->user()->currentAccessToken())->delete();

        return response()->json([
            'detail' => 'Logged out',
        ], 200);
    }

    public function helper_retrieve_alerts($_user_id)
    {
        $unassociated_alerts = Alert::leftJoin('alert_displays', function($x) use($_user_id) {
            $x->on('alerts.id', '=', 'alert_displays.alert_id')->where('user_id', '=', $_user_id);
        })
            ->where('audience', 'LIKE', 'first-time-%')
            ->whereNull('alert_displays.id')
            ->where(function($x) {
                $x->whereNull('live_at')->orWhere('live_at', '<', Carbon::now());
            })
            ->where(function($x) {
                $x->whereNull('down_at')->orWhere('down_at', '>', Carbon::now());
            })
            ->select('alerts.id', 'short_message', 'long_message', 'style', 'audience')
            ->get();

        $associated_alerts = AlertDisplay::leftJoin('alerts', 'alerts.id', '=', 'alert_displays.alert_id')
            ->where([['user_id', '=', $_user_id], ['dismissed', '=', 0]])
            ->where(function($x) {
                $x->whereNull('live_at')->orWhere('live_at', '<', Carbon::now());
            })
            ->where(function($x) {
                $x->whereNull('down_at')->orWhere('down_at', '>', Carbon::now());
            })
            ->whereNull('alerts.deleted_at')
            ->select('alerts.id', 'short_message', 'long_message', 'style', 'audience')
            ->get();

        return $unassociated_alerts->merge($associated_alerts)->keyBy('audience');
    }

    public function acknowledge_alert(Request $_r) {

        Notification::route('slack', config('creatia.webhook.goliath'))->notify(
            new SlackNotification('Agreement acknowledgement. User: ' . Auth::user()->email . ' has accepted agreement #' . $_r->input('alert'))
        );

        // Check if the alert does not already exist
        if($existing = AlertDisplay::where([['alert_id', '=', $_r->input('alert')], ['user_id', '=', Auth::user()->id]])->first()) {
            // Exists, return error
            if($existing->dismissed == 0) {
                $existing->dismissed = 1;
                $existing->save();
            }
        }
        else {
            // Does not exist, return 200

            $new = new AlertDisplay([
                'user_id' => Auth::user()->id,
                'alert_id' => $_r->input('alert')
            ]);

            $new->dismissed = 1;

            $new->save();
        }

        return response()->json([
            'detail' => 'SUCCESS'
        ], 200);
    }
}
