<?php

namespace App\Http\Controllers\Provisioning;

// Laravel
use App\Http\Controllers\BaseController;
use App\Models\Asns;
use App\Models\AsSets;
use App\Models\Organisation;
use App\Models\OrganisationExternalIdentities;
use App\Models\StateTracker;
use App\Rules\Fragments\PagedTable;
use App\Rules\OwnedAsnIdentity;
use App\Rules\OwnedAsSetIdentity;
use App\Rules\SidHash;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;

use Illuminate\Support\Facades\RateLimiter;

use Illuminate\Support\Str;

class TransitController extends BaseController
{

    public function __construct()
    {        
        parent::__construct();
    }

    /**
     * Endpoints that directly interact with the microservice
     */
    protected function helper_get_ports(int $_cxid) {
        
        return Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->retry(3, 100)
        ->get(env('bgp_microservice_v2') . "/custview/cxid/$_cxid")
        ->json();
    }

    protected function helper_get_port_details(string $_uuid) {

        return Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->retry(3, 100)
        ->get(env('bgp_microservice_v2') . "/custview/live/$_uuid")
        ->json();
    }

    protected function helper_get_asn(int $_asn, string $_sid, string $_uuid) {

        return Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->retry(3, 100)
        ->get(env('bgp_microservice_v2') . "/peeringdb/asn/$_asn", [
            'sid' => $_sid,
            'temp_id' => $_uuid
        ])
        ->json();
    }

    protected function helper_provision_session(string $_uuid, string $_interface, int $_asn, ?string $_as_set, string $_filter, ?int $_vlan_id, string $_ipv4_hash, string $_ipv6_hash, int $_max_prefix_4, int $_max_prefix_6, bool $_bfd, ?string $_password, string $_cxid, string $_username) {
        // Session creation shares the one change-per-minute limiter with every
        // other change-inducing request.
        $limiterKey = 'bgp_change_rate_limit' . Auth::user()->id;

        $response = null;

        $executed = RateLimiter::attempt(
            $limiterKey,
            $perMinute = 1,
            function() use(&$response, $_uuid, $_interface, $_asn, $_as_set, $_filter, $_vlan_id, $_ipv4_hash, $_ipv6_hash, $_max_prefix_4, $_max_prefix_6, $_bfd, $_password, $_cxid, $_username) {
        
                $response = Http::withHeaders([
                        'Content-Type' => 'application/json'
                    ])
                    ->post(env('bgp_microservice_v2') . "/custview/$_uuid/bgp", [
                        "interface_name"    => $_interface,
                        "asn"               => $_asn,
                        "as_set"            => $_as_set,
                        "filter"            => $_filter ,
                        "vlan_id"           => $_vlan_id,
                        "ipv4_hash"         => $_ipv4_hash,
                        "ipv6_hash"         => $_ipv6_hash,
                        "max_prefix_4"      => $_max_prefix_4,
                        "max_prefix_6"      => $_max_prefix_6,
                        "bfd"               => $_bfd,
                        "bgp_password"      => $_password,
                        "org_id"            => $_cxid,
                        "username"          => $_username
                    ])
                    ->json();

            }, 60
        );

        return $executed != true ? [
            'message' => 'rate-limit'
        ] : ( $response === null ? [
            'message' => 'failure'
        ] : $response );
    }

    protected function helper_get_job(string $_job) {
        
        return Http::withHeaders([
                    'Content-Type' => 'application/json'
                ])
                ->retry(3, 100)
                ->get(env('bgp_microservice_v2') . "/custview/job/$_job")
                ->json();   
    }

    protected function helper_ratelimited_request(string $_uuid, string $_path, array $_payload) {

        $response = null;

        $post = function() use(&$response, $_uuid, $_path, $_payload) {

            $response = Http::withHeaders([
                    'Content-Type' => 'application/json'
                ])
                ->post(env('bgp_microservice_v2') . "/custview/$_uuid/$_path", $_payload)
                ->json();
        };

        $executed = RateLimiter::attempt(
            'bgp_change_rate_limit' . Auth::user()->id,
            1,
            $post, 60
        );

        if($executed != true) {
            return [ 'message' => 'rate-limit' ];
        }

        return $response === null ? [
            'message' => 'failure'
        ] : $response;
    }

    protected function helper_session_config(string $_uuid, array $_changes) {
        return $this->helper_ratelimited_request($_uuid, 'session-config', [ ...$_changes, ...$this->get_attribution() ]);
    }

    protected function helper_delete_session(string $_uuid, string $_interface) {
        return $this->helper_ratelimited_request($_uuid, 'bgp/delete', [
            'interface_name' => $_interface,
            ...$this->get_attribution()
        ]);
    }

    protected function helper_get_port_config(string $_uuid) {

        return Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->retry(3, 100)
        ->get(env('bgp_microservice_v2') . "/custview/$_uuid/portconfig")
        ->json();
    }

    protected function helper_set_lp_limit(string $_uuid, ?int $_shape_rate) {
        return $this->helper_ratelimited_request($_uuid, 'lplimit', [
            'shape_rate' => $_shape_rate
        ]);
    }

    protected function helper_lag_member(string $_uuid, string $_port_channel, string $_interface, string $_action) {
        return $this->helper_ratelimited_request($_uuid, 'portconfig/lag/member', [
            'port_channel'  => $_port_channel,
            'interface'     => $_interface,
            'action'        => $_action
        ]);
    }

    protected function helper_lag_create(string $_uuid, string $_interface) {
        return $this->helper_ratelimited_request($_uuid, 'portconfig/lag/create', [
            'interface' => $_interface
        ]);
    }

    // The user description is a portal-only label - it never reaches the router,
    // so it is NOT run through the shared change rate-limiter. The custview
    // endpoint's field is `user_description` (temp_id in the path scopes the
    // port); null clears it. Attribution rides along for the microservice's
    // Slack log.
    protected function helper_set_user_description(string $_uuid, ?string $_description) {
        $response = Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->post(env('bgp_microservice_v2') . "/custview/$_uuid/userdescription", [
            'user_description' => $_description,
            ...$this->get_attribution()
        ]);

        // Unlike lplimit/lag/member (which answer { success: true }), this endpoint
        // returns the saved label + netbox targets and NO success flag - a 2xx IS the
        // success signal (422 on validation failure). Synthesize the flag the caller
        // checks from the HTTP status so a genuine save is not reported as a 500.
        return [ ...($response->json() ?? []), 'success' => $response->successful() ];
    }

    protected function helper_create_target(string $_uuid, int $_vlan, string $_interface) {
        return $this->helper_ratelimited_request($_uuid, 'target-id', [
            'vlan'           => $_vlan,
            'interface_name' => $_interface
        ]);
    }

    protected function helper_get_target(string $_uuid, string $_code) {

        return Http::withHeaders([
            'Content-Type' => 'application/json'
        ])
        ->retry(3, 100)
        ->get(env('bgp_microservice_v2') . "/custview/$_uuid/target-id/$_code")
        ->json();
    }

    protected function helper_create_evc(string $_uuid, array $_ends) {
        return $this->helper_ratelimited_request($_uuid, 'evc', [ ...$_ends, ...$this->get_attribution() ]);
    }

    protected function helper_delete_evc(string $_uuid, int $_evc_id, ?string $_interface) {
        return $this->helper_ratelimited_request($_uuid, "evc/$_evc_id/delete", [
            'interface_name' => $_interface,
            ...$this->get_attribution()
        ]);
    }

    /**
     * Little helpers in general
     */

    protected function get_id_hash($_id, $_name) {
        return hash('sha512', "$_id-$_name");
    }

    protected function get_id_key($_hash) {
        return 'U:' . Auth::user()->id . ":P:$_hash";
    }

    protected function get_attribution() {
        return [
            'org_id'    => Organisation::find(Auth::user()->organisation_id)->name,
            'username'  => Auth::user()->name
        ];
    }

    // A job handle stored as {hash}-{user}-{context}: the hash is an opaque token
    // seeded from the user, context and a microtime nonce, so it is globally unique
    // and unguessable. Only the hash ever leaves the backend; ownership is enforced
    // by re-deriving the -{user}- segment from Auth at lookup, never from the client.
    protected function make_job_key(string $_context): array {

        $hash = hash('sha512', Auth::user()->id . '|' . $_context . '|' . microtime(true) . '|' . Str::random(16));

        return [ $hash, $hash . '-' . Auth::user()->id . '-' . $_context ];
    }

    // Fixed 128-hex hash + '-' + numeric user id + '-' locates a caller's job unambiguously.
    protected function job_lookup_prefix(string $_hash): string {
        return $_hash . '-' . Auth::user()->id . '-';
    }

    // The context (e.g. session-delete:Ethernet2/1.1420) is the tail after the
    // {hash}-{user}- prefix. Callers only pass identifiers already bound to Auth,
    // so the current user's id length is the right offset.
    protected function job_context(string $_identifier): string {
        return substr($_identifier, 128 + 1 + strlen((string) Auth::user()->id) + 1);
    }

    protected function respond_with_job(array $_result, string $_identity, string $_context) {

        if(isset($_result['job_id'])) {

            // The scan snapshot behind this identity is stale once a change is in flight - the front-end must re-scan
            Redis::del($this->get_id_key($_identity));

            [ $hash, $identifier ] = $this->make_job_key($_context);

            StateTracker::create([
                'identifier'    => $identifier,
                'detail'        => $_result['job_id']
            ]);

            return response()->json([
                'circuit' => $_identity,
                'job' => $hash
            ], 200);
        }

        if(($_result['message'] ?? '') === 'rate-limit') {
            return response()->json([], 429);
        }

        // Diagnostic: a job-queued op reached here with no job_id and no rate-limit -
        // capture what the microservice actually returned so a contract drift (e.g.
        // bgp/delete no longer answering { job_id }) is visible in the log instead of
        // just surfacing as a generic 500. Remove once the delete shape is confirmed.
        \Log::warning('TransitController->respond_with_job: no job_id in microservice response', [
            'context' => $_context,
            'keys'    => array_keys($_result),
            'result'  => $_result
        ]);

        return response()->json([], 500);
    }

    /**
     * The meat of the controller
     */

    public function get_port_list(Request $_r) {

        // Validate input information.
        $valid_request = !Validator::make($_r->all(), PagedTable::rules(['community', 'description', 'type']))->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.422_INVALID_PAYLOAD')], 422);
        }

        // Get the users WHMCS ID
        $ids = OrganisationExternalIdentities::where([['key', '=', 'gsl'], ['organisation_id', '=', Auth::user()->organisation_id]])->get();

        if($ids->isEmpty()) {
            return response()->json([
                'draw' => (int)$_r->input('draw', 0),
                'recordsTotal' => 0,
                'recordsFiltered' => 0,
                'data' => []
            ], 200);
        }

        // Use it to make calls to our provisioning system

        $ports = [];

        foreach($ids as $id) {
            $ports = [...$ports, ...($this->helper_get_ports((int)$id->identity)['ports'] ?? [])];
        }

        // Top-level interfaces become rows carrying their full port data; lag members group under their port-channel

        $processed_rows = [];
        $lag_members = [];

        foreach($ports as $i => $port) {

            // Cache the temporary identifiers against a distinct identifiable. We'll just use a session.
            $redis_hash = $this->get_id_hash($i, $port['key']);

            Redis::setex($this->get_id_key($redis_hash), 600, $port['temp_id']);

            $port['id'] = $redis_hash;
            unset($port['temp_id']);

            if($port['parent_po'] !== null) {
                $lag_members[$port['router_name']][$port['parent_po']][] = $port;
            }
            else {
                $processed_rows[] = $port;
            }
        }

        foreach($processed_rows as &$row) {
            $row['lag_members'] = $lag_members[$row['router_name']][$row['interface_name']] ?? [];
        }
        unset($row);

        return response()->json([
            'draw' => (int)$_r->input('draw', 0),
            'recordsTotal' => sizeof($processed_rows),
            'recordsFiltered' => sizeof($processed_rows),
            'data' => $processed_rows
        ], 200);
    }

    public function get_port_details(Request $_r, string $_id) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( [ 'uuid' => $_id ], [
            'uuid'    => ['required', 'string', new SidHash]
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Take a distinct identifiable, check the session for this information, and then use the pulled out UUID to get the SID details.
        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            // Use it to make calls to our provisioning system
            $raw_rows = $this->helper_get_port_details($uuid);

            $processed_parentals = [];
            $processed_children = [];
            $processed_sessions = [];

            foreach($raw_rows as $row) {

                unset($row['temp_id']);

                $parent_name = $row['parent_interface_name'];

                if($parent_name === null) {
                    $processed_parentals[$row['interface_name']] = $row;
                }
                else {
                    $processed_children[$parent_name][$row['interface_name']] = $row;
                }

                // Sessions roll up under the top-level interface
                $session_key = $parent_name ?? $row['interface_name'];

                if(!empty($row['details']['bgp_details'])) {
                    $processed_sessions[$session_key] = [
                        ...($processed_sessions[$session_key] ?? []),
                        ...$row['details']['bgp_details']
                    ];
                }
            }
            
            return response()->json([
                'parent'    => $processed_parentals,
                'children'  => $processed_children,
                'bgp'       => $processed_sessions
            ], 200);
        }

        return response()->json([], 400);
    }

    public function get_asn_details(Request $_r) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( $_r->all(), [
            'asn_id'    => ['required', OwnedAsnIdentity::make(true)],
            'sid'       => ['required', 'string'],
            'identity'  => ['required', 'string', new SidHash],
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Take a distinct identifiable, check the session for this information, and then use the pulled out UUID to get the SID details.
        if(($uuid = Redis::get($this->get_id_key($_r->input('identity')))) && $uuid !== null) {

            // Use it to make calls to our provisioning system
            $information = $this->helper_get_asn(Asns::find($_r->input('asn_id'))->asn, $_r->input('sid'), $uuid );
            
            return response()->json([
                'info' => $information,
            ], 200);
        }

        return response()->json([], 400);
    }
    
    public function add_bgp_session(Request $_r) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( $_r->all(), [
            'identity'          => ['required', 'string', new SidHash],
            'interface'         => ['required', 'string'],
            'v4_hash'           => ['required', 'string'],
            'v6_hash'           => ['required', 'string'],
            'asn_id'            => ['required', OwnedAsnIdentity::make(true)],
            'as_set_id'         => ['nullable', OwnedAsSetIdentity::make(true)],
            'filter'            => ['required', Rule::in(['full', 'full-nodefault', 'default', 'denyall'])],
            'vlan'              => ['nullable', 'integer', 'min:2', 'max:4094'],
            'v4_max'            => ['nullable', 'integer', 'min:0'],
            'v6_max'            => ['nullable', 'integer', 'min:0'],
            'bfd'               => ['required', 'boolean'],
            'password'          => ['nullable', 'alpha_num:ascii'],
        ])->fails();

        $this->log_event('TransitController->add_bgp_session', $_r->except(['password']), 'BGP Session Submitted');

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Take a distinct identifiable, check the session for this information, and then use the pulled out UUID to get the SID details.
        if(($uuid = Redis::get($this->get_id_key($_r->input('identity')))) && $uuid !== null) {

            $asn = Asns::find($_r->input('asn_id'))->asn;

            $v4_max = max(1, (int)$_r->input('v4_max', 1));
            $v6_max = max(1, (int)$_r->input('v6_max', 1));

            $as_set = null; //$_r->input('as_set_id') !== null ? AsSets::find($_r->input('as_set_id'))->as_set : null;

            $session_inputs = [
                $uuid, $_r->input('interface'), $asn, $as_set, $_r->input('filter'), $_r->input('vlan'), $_r->input('v4_hash'), $_r->input('v6_hash'), $v4_max, $v6_max, $_r->input('bfd'), $_r->input('password'), ...array_values($this->get_attribution())
            ];

            $session_add = $this->helper_provision_session(...$session_inputs);

            $this->log_event(
                'TransitController->add_bgp_session',
                [
                    'inputs' => $_r->except(['password']),
                    'result' => $session_add
                ]
            );

            return $this->respond_with_job($session_add, $_r->input('identity'), 'session-create:' . $_r->input('interface'));
        }
        return response()->json([], 400);
    }

    public function get_existing_jobs(Request $_r) {

        $existing_jobs = StateTracker::where([
            ['identifier', 'LIKE', str_repeat('_', 128) . '-' . Auth::user()->id . '-%'],
            ['updated_at', '>', Carbon::now()->subHours(6)]
        ])->whereNotIn('detail', ['complete', 'failure'])->get()->transform(function($x) {

            try {
                $results = $this->helper_get_job($x->detail);

                if($results['status'] === 'running') {
                    return null;
                }
                elseif($results['status'] === 'complete') {
                    $x->detail = 'complete';
                    $x->save();
                }
                else {
                    $x->detail = 'failure';
                    $x->save();
                }
            }
            catch(\Throwable $t) {
                return null;
            }

            return [
                'status'  => $x->detail,
                'context' => $this->job_context($x->identifier),
                'job'     => substr($x->identifier, 0, 128)
            ];
        })->filter()->values();

        return response()->json($existing_jobs, 200);
    }

    // Button endpoints - all ride the consolidated session-config job queue
    public function update_bfd(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'enable'    => ['required', 'boolean'],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_session_config($uuid, [
                'interface_name' => $_r->input('interface'),
                'bfd'            => boolval($_r->input('enable'))
            ]);

            $this->log_event('TransitController->update_bfd', $_r->all(), 'BFD Change Submitted');

            return $this->respond_with_job($result, $_id, 'bfd:' . $_r->input('interface'));
        }

        return response()->json([], 400);
    }

    public function update_bgp_null(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'enable'    => ['required', 'boolean'],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_session_config($uuid, [
                'interface_name' => $_r->input('interface'),
                'nulling'        => boolval($_r->input('enable'))
            ]);

            $this->log_event('TransitController->update_bgp_null', $_r->all(), 'Null-Routing Change Submitted');

            return $this->respond_with_job($result, $_id, 'nulling:' . $_r->input('interface'));
        }

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

    }

    public function update_password(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'password'  => ['nullable', 'alpha_num:ascii'],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_session_config($uuid, [
                'interface_name'  => $_r->input('interface'),
                'change_password' => true,
                'password'        => $_r->input('password')
            ]);

            $this->log_event('TransitController->update_password', $_r->except(['password']), 'Session Password Change Submitted');

            return $this->respond_with_job($result, $_id, 'password:' . $_r->input('interface'));
        }

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

    }

    public function update_policy(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'interface' => ['required', 'string'],
            'filter'    => ['required', Rule::in(['full', 'full-nodefault', 'default', 'denyall'])]
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_session_config($uuid, [
                'interface_name' => $_r->input('interface'),
                'filter'         => $_r->input('filter')
            ]);

            $this->log_event('TransitController->update_policy', $_r->all(), 'BGP Export Policy Change Submitted');

            return $this->respond_with_job($result, $_id, 'policy:' . $_r->input('interface'));
        }

        return response()->json([], 400);
    }

    public function refresh_limit(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_session_config($uuid, [
                'interface_name'     => $_r->input('interface'),
                'refresh_max_prefix' => true
            ]);

            $this->log_event('TransitController->refresh_limit', $_r->all(), 'BGP Max-Prefix Refresh Submitted');

            return $this->respond_with_job($result, $_id, 'max-prefix:' . $_r->input('interface'));
        }

        return response()->json([], 400);
    }

    public function delete_session(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_delete_session($uuid, $_r->input('interface'));

            $this->log_event('TransitController->delete_session', $_r->all(), 'BGP Session Deletion Submitted');

            return $this->respond_with_job($result, $_id, 'session-delete:' . $_r->input('interface'));
        }

        return response()->json([], 400);
    }

    public function get_job(Request $_r, string $_key) {

        // The client only ever holds the opaque hash - reject anything that is not one
        if(preg_match('/^[a-f0-9]{128}$/', $_key) !== 1) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Ownership is enforced here: the -{user}- segment is bound from Auth, not the client,
        // so another caller's hash resolves to no row of ours.
        $job = StateTracker::where('identifier', 'LIKE', $this->job_lookup_prefix($_key) . '%')->orderBy('id', 'DESC')->first();

        if($job === null) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        $generic = [
            'running'   => 'Provisioning in progress',
            'complete'  => 'Provisioning complete',
            'failure'   => 'Provisioning failed - our team has been notified'
        ];

        $status = in_array($job->detail, ['complete', 'failure']) ? $job->detail : 'running';

        if($status === 'running') {

            try {
                $results = $this->helper_get_job($job->detail);

                if($results['status'] === 'complete') {
                    $status = 'complete';
                    $job->detail = 'complete';
                    $job->save();
                }
                elseif($results['status'] !== 'running') {
                    $status = 'failure';
                    $job->detail = 'failure';
                    $job->save();
                }
            }
            catch(\Throwable $t) {}
        }

        return response()->json([
            'status'  => $status,
            'message' => $generic[$status],
            'context' => $this->job_context($job->identifier),
            'job'     => $_key
        ], 200);
    }

    // Port configuration
    public function get_port_config(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id ], [
            'uuid'    => ['required', 'string', new SidHash]
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $config = $this->helper_get_port_config($uuid);

            return $config !== null ? response()->json($config, 200) : response()->json([], 500);
        }

        return response()->json([], 400);
    }

    public function update_lp_limit(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'       => ['required', 'string', new SidHash],
            'shape_rate' => ['present', 'nullable', 'integer', 'between:10,75']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $shape_rate = $_r->input('shape_rate');

            $response = $this->helper_set_lp_limit($uuid, $shape_rate !== null ? (int)$shape_rate : null);

            if(isset($response['success']) && $response['success'] === true) {
                return response()->json([], 200);
            }
            elseif($response['message'] === 'rate-limit') {
                return response()->json([], 429);
            }

            return response()->json([], 500);
        }

        return response()->json([], 400);
    }

    public function update_lag_member(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'          => ['required', 'string', new SidHash],
            'port_channel'  => ['required', 'string'],
            'interface'     => ['required', 'string'],
            'action'        => ['required', Rule::in(['add', 'remove'])]
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            // The last member cannot be removed - the service would go down
            if($_r->input('action') === 'remove') {

                $config = $this->helper_get_port_config($uuid);

                foreach(($config['lags'] ?? []) as $lag) {
                    if($lag['name'] === $_r->input('port_channel') && sizeof($lag['members'] ?? []) <= 1) {
                        return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
                    }
                }
            }

            $response = $this->helper_lag_member($uuid, $_r->input('port_channel'), $_r->input('interface'), $_r->input('action'));

            $this->log_event('TransitController->update_lag_member', $_r->all(), 'LAG Membership Change Submitted');

            if(isset($response['success']) && $response['success'] === true) {
                return response()->json([], 200);
            }
            elseif($response['message'] === 'rate-limit') {
                return response()->json([], 429);
            }

            return response()->json([], 500);
        }

        return response()->json([], 400);
    }

    public function create_lag(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $response = $this->helper_lag_create($uuid, $_r->input('interface'));

            $this->log_event('TransitController->create_lag', $_r->all(), 'LAG Creation Submitted');

            if(isset($response['success']) && $response['success'] === true) {
                return response()->json([], 200);
            }
            elseif($response['message'] === 'rate-limit') {
                return response()->json([], 429);
            }

            return response()->json([], 500);
        }

        return response()->json([], 400);
    }

    // Portal-only service label. Letters, numbers, spaces and dashes; empty
    // clears it. Not rate-limited - it never touches the router.
    public function set_user_description(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'             => ['required', 'string', new SidHash],
            'user_description' => ['present', 'nullable', 'string', 'max:100', 'regex:/^[A-Za-z0-9 \-]*$/']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $description = $_r->input('user_description');

            $response = $this->helper_set_user_description($uuid, ($description === null || $description === '') ? null : $description);

            $this->log_event('TransitController->set_user_description', $_r->all(), 'User Description Set');

            if(isset($response['success']) && $response['success'] === true) {
                return response()->json([], 200);
            }

            return response()->json([], 500);
        }

        return response()->json([], 400);
    }

    // Ethernet / MEF
    public function create_tag(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'vlan'      => ['required', 'integer', 'min:2', 'max:4094'],
            'interface' => ['required', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $response = $this->helper_create_target($uuid, (int)$_r->input('vlan'), $_r->input('interface'));

            $this->log_event('TransitController->create_tag', $_r->all(), 'Inbound Target Tag Created');

            if(isset($response['code'])) {
                return response()->json([
                    'code' => $response['code'],
                    'vlan' => $response['vlan'] ?? (int)$_r->input('vlan')
                ], 200);
            }
            elseif(($response['message'] ?? '') === 'rate-limit') {
                return response()->json([], 429);
            }

            return response()->json([], 500);
        }

        return response()->json([], 400);
    }

    public function get_tag(Request $_r, string $_id, string $_code) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, 'code' => $_code ], [
            'uuid'    => ['required', 'string', new SidHash],
            'code'    => ['required', 'string', 'max:64', 'alpha_dash:ascii']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $target = $this->helper_get_target($uuid, $_code);

            // Whitelist the response: the microservice includes the tag owner's
            // sid/device/interface, which must never reach the counterparty.
            return $target !== null ? response()->json([
                'status' => $target['status'] ?? 'unknown',
                'vlan'   => $target['vlan'] ?? null
            ], 200) : response()->json([], 500);
        }

        return response()->json([], 400);
    }

    public function create_evc(Request $_r, string $_id) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, ...$_r->all() ], [
            'uuid'          => ['required', 'string', new SidHash],
            'a_interface'   => ['required', 'string'],
            'a_vlan'        => ['required', 'integer', 'min:2', 'max:4094'],
            'z_id'          => ['required_without:target_code', 'prohibits:target_code', 'nullable', 'string', new SidHash],
            'z_interface'   => ['required_with:z_id', 'nullable', 'string'],
            'z_vlan'        => ['required_with:z_id', 'nullable', 'integer', 'min:2', 'max:4094'],
            'target_code'   => ['required_without:z_id', 'nullable', 'string', 'max:64', 'alpha_dash:ascii']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $ends = [
                'a_interface_name' => $_r->input('a_interface'),
                'a_vlan'           => (int)$_r->input('a_vlan')
            ];

            if($_r->input('target_code') !== null) {
                $ends['target_code'] = $_r->input('target_code');
            }
            else {

                // The Z end arrives as another of the caller's port hashes - resolve it the same way
                if(!(($z_uuid = Redis::get($this->get_id_key($_r->input('z_id')))) && $z_uuid !== null)) {
                    return response()->json([], 400);
                }

                $ends['z_temp_id']        = $z_uuid;
                $ends['z_interface_name'] = $_r->input('z_interface');
                $ends['z_vlan']           = (int)$_r->input('z_vlan');
            }

            $result = $this->helper_create_evc($uuid, $ends);

            $this->log_event('TransitController->create_evc', $_r->all(), 'EVC Provisioning Submitted');

            // Both ends of the circuit change - the Z end's cached identity goes stale too
            if(isset($result['job_id']) && isset($ends['z_temp_id'])) {
                Redis::del($this->get_id_key($_r->input('z_id')));
            }

            return $this->respond_with_job($result, $_id, 'evc-create:' . $_r->input('a_interface'));
        }

        return response()->json([], 400);
    }

    public function delete_evc(Request $_r, string $_id, string $_evc) {

        $valid_request = !Validator::make( [ 'uuid' => $_id, 'evc' => $_evc, ...$_r->all() ], [
            'uuid'      => ['required', 'string', new SidHash],
            'evc'       => ['required', 'integer', 'min:1'],
            'interface' => ['nullable', 'string']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        if(($uuid = Redis::get($this->get_id_key($_id))) && $uuid !== null) {

            $result = $this->helper_delete_evc($uuid, (int)$_evc, $_r->input('interface'));

            $this->log_event('TransitController->delete_evc', $_r->all(), 'EVC Deletion Submitted');

            return $this->respond_with_job($result, $_id, "evc-delete:$_evc");
        }

        return response()->json([], 400);
    }

}