<?php
namespace App\Http\Controllers\Token;

// Laravel Specifics
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

// Creatia Specifics
use App\Models\ApiToken;
use App\Models\ApiScope;
use App\Models\ApiTokenPermission;
use App\Models\ApiTokenWhitelist;
use App\Http\Controllers\BaseController;
use App\Models\ApiTokenBearer;
use App\Rules\Cidr;
use App\Rules\SafeName;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
use Throwable;

class Controller extends BaseController
{

    public function get_tokens(Request $_r) {
        $USER_ORGANISATION_ID = Auth::user()->organisation_id;

        return $this->paged_table($_r, function() use($USER_ORGANISATION_ID)  {
            return ApiToken::leftJoin('api_token_permissions', 'api_token_id', '=', 'api_tokens.id')
                ->leftJoin('api_scopes', 'api_scopes.id', '=', 'api_token_permissions.api_scopes_id')
                ->leftJoin('api_token_whitelist', 'api_token_whitelist.token_id', '=', 'api_tokens.id')
                ->select(
                    'api_tokens.id',
                    'api_tokens.name',
                    'api_tokens.identifier',
                    'api_tokens.active',
                    'api_tokens.created_at',
                    'api_tokens.updated_at',
                    DB::raw("string_agg(DISTINCT api_scopes.name, ',') as \"scopes\""),
                    DB::raw("string_agg(DISTINCT text(api_token_whitelist.address), ',') as \"whitelisted\""),
                    DB::raw("(SELECT COUNT(*) > 0 FROM api_token_bearer a WHERE a.token_id = api_tokens.id) as \"recently_used\"")
                )
                ->where('api_tokens.organisation_id', '=', $USER_ORGANISATION_ID)
                ->groupBy('api_tokens.id');
        }, [ 
            'name'          => [ 'db' => 'api_tokens.name', 'searchable' => true ],
            'identifier'    => [ 'db' => 'api_tokens.identifier', 'searchable' => true ],            
        ], 'api_tokens.name', null);
    }

    public function get_all_scopes(Request $_r) {
        try {
            return response()->json(ApiScope::select('id', 'name')->get()->toArray(), 200);
        } catch(Throwable $ex) {}

        return response()->json(['detail' => __('api.500_GENERIC')], 500);
    }

    public function get_token_details(Request $_r, $_token_id) {
        $ORG_ID = Auth::user()->organisation_id;

        try {
            if($x = ApiToken::where([['id', '=', $_token_id], ['organisation_id', '=', $ORG_ID]])->first()) {
                // Get ALL scopes
                $all_scopes = ApiScope::select('id', 'name')->get()->toArray();
                $specific_scopes = ApiTokenPermission::select('api_scopes_id')
                    ->where('api_token_id', '=', $x->id)
                    ->get()
                    ->pluck('api_scopes_id')
                    ->toArray();

                $whitelisted_endpoints = ApiTokenWhitelist::select('address')
                    ->where('token_id', '=', $x->id)
                    ->get()
                    ->pluck('address')
                    ->toArray();

                return response()->json([
                    'name' => $x->name,
                    'all_scopes' => $all_scopes,
                    'used_scopes' => $specific_scopes,
                    'whitelist' => $whitelisted_endpoints
                ], 200);
            }
        } catch(Throwable $ex) {}

        return response()->json(['detail' => __('api.500_GENERIC')], 500);
    }

    public function post_token(Request $_r) {
        try {

            $valid_request = !Validator::make($_r->input(), [
                'name'          => ['required', new SafeName],
                'active'        => ['required', 'boolean'],
                'whitelist'     => ['required'],
                'whitelist.*'   => ['required', new Cidr(0)],
                'scope'         => ['required'],
                'scope.*'       => ['required', 'exists:api_scopes,id']
            ])->fails();

            if(!$valid_request) {
                return response()->json([], 400);
            }

            $outcome = DB::transaction(function () use( $_r) {
                // Generate new tokens until a valid identity token is found.
                $length = 32;

                $random_identity = Str::random($length);
                $random_password = Str::random($length);
                
                while(ApiToken::where('identifier', '=', $random_identity)->count()) {
                    $random_identity = Str::random($length++);
                    $random_password = Str::random($length);
                }

                $new_token = new ApiToken([
                    'name' => $_r->input('name'),
                    'identifier' => $random_identity,
                    'token' => Hash::make($random_password),
                    'active' => $_r->input('active', false) ? 1 : 0,
                    'organisation_id' => Auth::user()->organisation_id
                ]);

                if($new_token->save()) {
                    // Make whitelist
                    $whitelist_values = [];

                    foreach($_r->input('whitelist') as $whitelist_address) {
                        $whitelist_values = [
                            'address' => $whitelist_address,
                            'token_id' => $new_token->id,
                            'organisation_id' => Auth::user()->organisation_id
                        ];
                    }

                    if(DB::table('api_token_whitelist')->insert($whitelist_values)) {
                        // Make scopes
                        $scope_values = [];

                        foreach($_r->input('scope') as $scope) {
                            $scope_values = [
                                'api_token_id' => $new_token->id,
                                'api_scopes_id' => $scope
                            ];
                        }

                        if(DB::table('api_token_permissions')->insert($scope_values)) {
                            return [
                                'identity' => $random_identity,
                                'secret' => $random_password
                            ];
                        }
                    }
                }

                return false;
            });

            if($outcome) {
                return response()->json([
                    'identity' => $outcome['identity'],
                    'secret' => $outcome['secret']
                ], 200);
            }
        } catch(Throwable $ex) {}

        return response()->json(['detail' => __('api.500_GENERIC')], 500);
    }

    public function patch_token(Request $_r, $_token_id) {
        try {

            $valid_request = !Validator::make($_r->input(), [
                'name'          => ['required', new SafeName],
                'whitelist'     => ['required'],
                'whitelist.*'   => [new Cidr(0)],
                'scope'         => ['required'],
                'scope.*'       => ['exists:api_scopes,id']
            ])->fails();

            if(!$valid_request) {
                return response()->json([], 400);
            }

            if($existing_token = ApiToken::whereOwned()->where('id', '=', $_token_id)->first()) {
                $outcome = DB::transaction(function() use( $_r, $existing_token) {
                    $existing_token->name = $_r->input('name');

                    if($existing_token->save()) {
                        // Delete old whitelist values
                        ApiTokenWhitelist::where('token_id', '=', $existing_token->id)->delete();
    
                        foreach($_r->input('whitelist') as $whitelist_address) {
                            if(!ApiTokenWhitelist::create([
                                'address' => $whitelist_address,
                                'token_id' => $existing_token->id,
                                'organisation_id' => Auth::user()->organisation_id
                            ])) {
                                return false;
                            }
                        }
                        
                        // Delete old scope values
                        ApiTokenPermission::where('api_token_id', '=', $existing_token->id)->delete();

                        foreach($_r->input('scope') as $scope) {
                            if(!ApiTokenPermission::create([
                                'api_token_id' => $existing_token->id,
                                'api_scopes_id' => $scope
                            ])) {
                                return false;
                            }
                        }

                        return true;
                    }
    
                    return false;
                });
            }

            if($outcome) {
                // Drop the cached middleware state so the new scopes apply immediately.
                Redis::del("api-token-$existing_token->id");

                return response()->json(['detail' => __('api.200_GENERIC')], 200);
            }
        } catch(Throwable $ex) {}

        return response()->json(['detail' => __('api.500_GENERIC')], 500);
    }

    public function delete_token(Request $_r, $_token_id) {
        try {
            if($existing_token = ApiToken::whereOwned()->where('id', '=', $_token_id)->first()) {

                DB::beginTransaction();

                // Delete whitelist values
                ApiTokenWhitelist::where('token_id', '=', $existing_token->id)->delete();

                // Delete scopes
                ApiTokenPermission::where('api_token_id', '=', $existing_token->id)->delete();

                // Delete bearer tokens
                ApiTokenBearer::where('token_id', '=', $existing_token->id)->delete();

                // Delete the API token
                ApiToken::where('id', '=',  $existing_token->id)->delete();

                DB::commit();

                // Drop the cached existence check so the deleted token stops authenticating immediately.
                Redis::del("api-token-$existing_token->id");

                return response()->json(['detail' => __('api.200_GENERIC')], 200);
            }
        }
        catch(Throwable $ex) {
            DB::rollBack();
        }

        return response()->json(['detail' => __('api.500_GENERIC')], 500);
    }

    public function toggle_token(Request $_r, int $_token_id) {

        try {
            if($existing_token = ApiToken::whereOwned()->where('id', '=', $_token_id)->first()) {
                $existing_token->active = $_r->input('activate') ? 1 : 0;

                if($existing_token->save()) {
                    Redis::del("api-token-$existing_token->id");

                    return response()->json(['detail' => __('api.200_GENERIC')], 200);
                }
            }
        }
        catch(Throwable $ex) {}

        return response()->json(['detail' => __('api.500_GENERIC')], 500);

    }

}
