<?php

namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;

use Fleetbase\Exceptions\FleetbaseRequestValidationException;
use Fleetbase\FleetOps\Exports\DriverExport;
use Fleetbase\FleetOps\Http\Controllers\Api\v1\DriverController as ApiDriverController;
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
use Fleetbase\FleetOps\Http\Requests\Internal\CreateDriverRequest;
use Fleetbase\FleetOps\Http\Requests\Internal\UpdateDriverRequest;
use Fleetbase\FleetOps\Http\Resources\v1\Index\Order as IndexOrderResource;
use Fleetbase\FleetOps\Imports\DriverImport;
use Fleetbase\FleetOps\Models\Driver;
use Fleetbase\FleetOps\Models\Order;
use Fleetbase\FleetOps\Models\Vehicle;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\Http\Requests\ExportRequest;
use Fleetbase\Http\Requests\ImportRequest;
use Fleetbase\LaravelMysqlSpatial\Types\Point;
use Fleetbase\Models\Invite;
use Fleetbase\Models\User;
use Fleetbase\Models\VerificationCode;
use Fleetbase\Support\Auth;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;

class DriverController extends FleetOpsController
{
    use Traits\DriverSchedulingTrait;
    /**
     * The resource to query.
     *
     * @var string
     */
    public $resource = 'driver';

    /**
     * Creates a record with request payload.
     *
     * @return \Illuminate\Http\Response
     */
    public function createRecord(Request $request)
    {
        $input = $request->input('driver');

        // Normalize vehicle field - the frontend may send the full vehicle object,
        // a UUID string, or a public_id string. Normalize to a single identifier
        // so the ResolvableVehicle rule can validate it correctly.
        if (isset($input['vehicle']) && is_array($input['vehicle'])) {
            $input['vehicle'] = data_get($input['vehicle'], 'id')
                ?? data_get($input['vehicle'], 'public_id')
                ?? data_get($input['vehicle'], 'uuid')
                ?? null;
            $request->merge(['driver' => $input]);
        }

        // create validation request
        $createDriverRequest = CreateDriverRequest::createFrom($request);
        $rules               = $createDriverRequest->rules();

        // manually validate request
        $validator = Validator::make($input, $rules);

        if ($validator->fails()) {
            // here if the user exists already
            // within organization: offer to create driver record
            // outside organization: invite to join organization AS DRIVER
            if ($validator->errors()->hasAny(['phone', 'email'])) {
                // get existing user
                $existingUser = null;

                // if values provided for user lookup
                if (!empty($input['phone']) || !empty($input['email'])) {
                    $existingUserQuery = User::query();

                    if (!empty($input['phone']) && is_string($input['phone'])) {
                        $existingUserQuery->orWhere(function ($q) use ($input) {
                            $q->where('phone', $input['phone'])->whereNotNull('phone');
                        });
                    }

                    if (!empty($input['email']) && is_string($input['email'])) {
                        $existingUserQuery->orWhere(function ($q) use ($input) {
                            $q->where('email', $input['email'])->whereNotNull('email');
                        });
                    }

                    $existingUser = $existingUserQuery->first();
                }

                if ($existingUser) {
                    // if exists in organization create driver profile for user
                    $isOrganizationMember = $existingUser->companies()->where('companies.uuid', session('company'))->exists();

                    // Check if driver profile also already exists
                    $existingDriverProfile = Driver::where(['company_uuid' => session('company'), 'user_uuid' => $existingUser->uuid])->first();
                    if ($existingDriverProfile) {
                        return ['driver' => new $this->resource($existingDriverProfile)];
                    }

                    // create driver profile for user
                    $input = collect($input)
                        ->except(['name', 'password', 'email', 'phone', 'meta', 'avatar_uuid', 'photo_uuid', 'status'])
                        ->filter()
                        ->toArray();

                    // Get current session company
                    $company               = Auth::getCompany();
                    $input['company_uuid'] = session('company', $company->uuid);
                    $input['user_uuid']    = $existingUser->uuid;
                    $input['slug']         = $existingUser->slug;

                    // If no location provided set
                    if (empty($input['location'])) {
                        $input['location'] = new Point(0, 0);
                    }

                    // create the profile
                    $driverProfile = Driver::create($input);

                    // If not already a member of the company assign them to the company and send the user an invite
                    if (!$isOrganizationMember && $company) {
                        $existingUser->assignCompany($company);
                    }

                    return ['driver' => new $this->resource($driverProfile)];
                }
            }

            // check from validator object if phone or email is not unique
            return $createDriverRequest->responseWithErrors($validator);
        }

        try {
            $record = $this->model->createRecordFromRequest(
                $request,
                function (&$request, &$input) {
                    $input = collect($input);

                    // Get current session company
                    $company                   = Auth::getCompany();
                    if (!$company) {
                        throw new \Exception('Unable to create driver.');
                    }

                    if ($input->has('user_uuid')) {
                        $user = User::where('uuid', $input->get('user_uuid'))->first();

                        // Check if a driver profile already exists for this user in the current company
                        if ($user) {
                            $existingDriver = Driver::where(['user_uuid' => $user->uuid, 'company_uuid' => session('company')])->first();
                            if ($existingDriver) {
                                throw new \Exception('This user account already belongs to a driver.');
                            }
                        }

                        // If user doesn't exist with provided UUID, create new user
                        if (!$user) {
                            $userInput = $input
                                ->only(['name', 'password', 'email', 'phone', 'status', 'avatar_uuid'])
                                ->filter()
                                ->toArray();

                            // handle `photo_uuid`
                            if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) {
                                $userInput['avatar_uuid'] = $input['photo_uuid'];
                            }

                            // Make sure password is set
                            if (empty($userInput['password'])) {
                                $userInput['password'] = Str::random(14);
                            }

                            // Set user company
                            $userInput['company_uuid'] = session('company', $company->uuid);

                            // Apply user infos
                            $userInput = User::applyUserInfoFromRequest($request, $userInput);

                            // Create user account
                            $user = User::create($userInput);

                            // Set the user type to driver
                            $user->setType('driver');
                        } elseif ($input->has('photo_uuid')) {
                            // Update existing user's avatar if photo provided
                            $user->update(['avatar_uuid' => $input->get('photo_uuid')]);
                        }
                    } else {
                        $userInput = $input
                            ->only(['name', 'password', 'email', 'phone', 'status', 'avatar_uuid'])
                            ->filter()
                            ->toArray();

                        // handle `photo_uuid`
                        if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) {
                            $userInput['avatar_uuid'] = $input['photo_uuid'];
                        }

                        // Make sure password is set
                        if (empty($userInput['password'])) {
                            $userInput['password'] = Str::random(14);
                        }

                        // Set user company
                        $userInput['company_uuid'] = session('company', $company->uuid);

                        // Apply user infos
                        $userInput = User::applyUserInfoFromRequest($request, $userInput);

                        // Create user account
                        $user = User::create($userInput);

                        // Set the user type to driver
                        $user->setType('driver');
                    }

                    // if exists in organization create driver profile for user
                    $isOrganizationMember = $user->companies()->where('companies.uuid', session('company'))->exists();

                    // Prepare input
                    $input = $input
                            ->except(['name', 'password', 'email', 'phone', 'meta', 'avatar_uuid', 'photo_uuid', 'status'])
                            ->filter()
                            ->toArray();

                    // Assign user to company and send invite
                    if (!$isOrganizationMember && $company) {
                        $user->assignCompany($company);
                    }

                    // Set user type as driver and set role to driver
                    if ($user->type === 'driver') {
                        $user->assignSingleRole('Driver');
                    }

                    $input['user_uuid'] = $user->uuid;
                    $input['slug']      = $user->slug;

                    // If no location provided set
                    if (empty($input['location'])) {
                        $input['location'] = new Point(0, 0);
                    }
                },
                function ($request, &$driver) {
                    $driver->load(['user']);
                    $customFieldValues = $request->array('driver.custom_field_values');
                    if ($customFieldValues) {
                        $driver->syncCustomFieldValues($customFieldValues);
                    }
                }
            );

            return ['driver' => new $this->resource($record)];
        } catch (QueryException $e) {
            return response()->error(env('DEBUG') ? $e->getMessage() : 'Error occurred while trying to create a ' . $this->resourceSingularlName);
        } catch (FleetbaseRequestValidationException $e) {
            return response()->error($e->getErrors());
        } catch (\Exception $e) {
            return response()->error($e->getMessage());
        }
    }

    /**
     * Updates a record with request payload.
     *
     * @return \Illuminate\Http\Response
     */
    public function updateRecord(Request $request, string $id)
    {
        // get input data
        $input = $request->input('driver');

        // Normalize vehicle field - the frontend may send the full vehicle object,
        // a UUID string, or a public_id string. Normalize to a single identifier
        // so the ResolvableVehicle rule can validate it correctly.
        if (isset($input['vehicle']) && is_array($input['vehicle'])) {
            $input['vehicle'] = data_get($input['vehicle'], 'id')
                ?? data_get($input['vehicle'], 'public_id')
                ?? data_get($input['vehicle'], 'uuid')
                ?? null;
            $request->merge(['driver' => $input]);
        }

        // create validation request
        $updateDriverRequest = UpdateDriverRequest::createFrom($request);
        $rules               = $updateDriverRequest->rules();

        // manually validate request
        $validator = Validator::make($input, $rules);

        if ($validator->fails()) {
            return $updateDriverRequest->responseWithErrors($validator);
        }

        try {
            $record = $this->model->updateRecordFromRequest(
                $request,
                $id,
                function (&$request, &$driver, &$input) {
                    $driver->load(['user'])->guard(['user_uuid']);
                    $input     = collect($input);
                    $userInput = $input->only(['name', 'password', 'email', 'phone', 'avatar_uuid'])->reject(fn ($value) => $value === null)->toArray();
                    // handle `photo_uuid`
                    if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) {
                        $userInput['avatar_uuid'] = $input['photo_uuid'];
                    }
                    $input     = $input->except(['name', 'password', 'email', 'phone', 'meta', 'avatar_uuid', 'photo_uuid'])->toArray();

                    // Update driver user details
                    $driverUser = $driver->getUser();
                    if ($driverUser && !empty($userInput)) {
                        $driverUser->update($userInput);
                    }

                    // Flush cache
                    $driver->flushAttributesCache();
                },
                function ($request, &$driver) {
                    $driver->load(['user']);
                    if ($driver->user) {
                        $driver->user->setHidden(['driver']);
                    }

                    $driver->setHidden(['user']);
                    $customFieldValues = $request->array('driver.custom_field_values');
                    if ($customFieldValues) {
                        $driver->syncCustomFieldValues($customFieldValues);
                    }
                }
            );

            return ['driver' => new $this->resource($record)];
        } catch (QueryException $e) {
            return response()->error(env('DEBUG') ? $e->getMessage() : 'Error occurred while trying to update a ' . $this->resourceSingularlName);
        } catch (FleetbaseRequestValidationException $e) {
            return response()->error($e->getErrors());
        } catch (\Exception $e) {
            return response()->error($e->getMessage());
        }
    }

    /**
     * Get all status options for an driver.
     *
     * @return \Illuminate\Http\Response
     */
    public function statuses()
    {
        $statuses = DB::table('drivers')
            ->select('status')
            ->where('company_uuid', session('company'))
            ->distinct()
            ->get()
            ->pluck('status')
            ->filter()
            ->values();

        return response()->json($statuses);
    }

    /**
     * Get all avatar options for an vehicle.
     *
     * @return \Illuminate\Http\Response
     */
    public function avatars()
    {
        $options = Driver::getAvatarOptions();

        return response()->json($options);
    }

    /**
     * Export the drivers to excel or csv.
     *
     * @return \Illuminate\Http\Response
     */
    public static function export(ExportRequest $request)
    {
        $format       = $request->input('format', 'xlsx');
        $selections   = $request->array('selections');
        $fileName     = trim(Str::slug('drivers-' . date('Y-m-d-H:i')) . '.' . $format);

        return Excel::download(new DriverExport($selections), $fileName);
    }

    /**
     * Assigns a driver to a specified order.
     *
     * @param Fleetbase\FleetOps\Http\Requests\Internal\AssignOrderRequest $request
     *
     * @return \Illuminate\Http\Response $response
     */
    public function assignOrder(Request $request, ?string $id = null)
    {
        $request->validate([
            'driver' => ($id ? 'nullable' : 'required') . '|string',
            'order'  => 'required|string',
        ]);

        $driverIdentifier = $id ?? $request->input('driver');
        $orderIdentifier  = $request->input('order');

        $driver = $this->findDriver($driverIdentifier);
        $order  = $this->findOrder($orderIdentifier);

        if ($order->hasDriverAssigned) {
            return response()->error('A driver is already assigned to this order.');
        }

        if ($order->isDriver($driver)) {
            return response()->error('The driver is already assigned to this order.');
        }

        $order->assignDriver($driver);
        $driver->update(['current_job_uuid' => $order->uuid]);

        return response()->json([
            'status'  => 'ok',
            'message' => 'Driver assigned',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'order'   => $order->fresh(['driverAssigned']),
        ]);
    }

    public function assignedOrders(string $id): JsonResponse
    {
        $driver = $this->findDriver($id);
        $orders = Order::where('driver_assigned_uuid', $driver->uuid)
            ->where('company_uuid', session('company'))
            ->with(['payload', 'trackingNumber', 'orderConfig', 'driverAssigned', 'vehicleAssigned'])
            ->orderByRaw('uuid = ? desc', [$driver->current_job_uuid])
            ->latest()
            ->get();

        return response()->json([
            'status'  => 'ok',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'orders'  => IndexOrderResource::collection($orders)->resolve(),
            'current' => $driver->current_job_uuid,
            'count'   => $orders->count(),
        ]);
    }

    public function unassignOrders(Request $request, string $id): JsonResponse
    {
        $request->validate([
            'orders'   => 'required|array|min:1',
            'orders.*' => 'required|string',
        ]);

        $driver = $this->findDriver($id);
        $ids    = collect($request->input('orders'))->filter()->unique()->values();
        $orders = Order::where('driver_assigned_uuid', $driver->uuid)
            ->where('company_uuid', session('company'))
            ->where(function ($query) use ($ids) {
                $query->whereIn('uuid', $ids)->orWhereIn('public_id', $ids);
            })
            ->get();

        if ($orders->isEmpty()) {
            return response()->error('No assigned orders were selected for this driver.');
        }

        DB::transaction(function () use ($driver, $orders): void {
            Order::whereIn('uuid', $orders->pluck('uuid'))->update([
                'driver_assigned_uuid' => null,
                'updated_at'           => now(),
            ]);

            if ($driver->current_job_uuid && $orders->contains('uuid', $driver->current_job_uuid)) {
                $driver->update(['current_job_uuid' => null]);
            }
        });

        return response()->json([
            'status'  => 'ok',
            'message' => 'Driver unassigned from selected orders.',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'orders'  => IndexOrderResource::collection($orders->fresh(['driverAssigned', 'vehicleAssigned']))->resolve(),
            'count'   => $orders->count(),
        ]);
    }

    public function unassignOrder(string $id): JsonResponse
    {
        $driver = $this->findDriver($id);
        $order  = Order::where('driver_assigned_uuid', $driver->uuid)
            ->where('company_uuid', session('company'))
            ->first() ?? $driver->getCurrentOrder();

        if ($order) {
            $order->update(['driver_assigned_uuid' => null]);
        }

        $driver->unassignCurrentJob();

        return response()->json([
            'status'  => 'ok',
            'message' => 'Driver unassigned from order.',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'order'   => $order?->fresh(['driverAssigned']),
        ]);
    }

    public function assignVehicle(Request $request, string $id): JsonResponse
    {
        $request->validate(['vehicle' => 'required|string']);

        $driver  = $this->findDriver($id);
        $vehicle = $this->findVehicle($request->input('vehicle'));

        $driver->assignVehicle($vehicle);

        return response()->json([
            'status'  => 'ok',
            'message' => 'Vehicle assigned to driver.',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'vehicle' => $vehicle->fresh(['driver']),
        ]);
    }

    public function unassignVehicle(string $id): JsonResponse
    {
        $driver  = $this->findDriver($id);
        $vehicle = $driver->vehicle;

        $driver->update(['vehicle_uuid' => null]);

        return response()->json([
            'status'  => 'ok',
            'message' => 'Vehicle unassigned from driver.',
            'driver'  => $driver->fresh(['vehicle', 'currentOrder']),
            'vehicle' => $vehicle?->fresh(['driver']),
        ]);
    }

    protected function findDriver(?string $id): Driver
    {
        return Driver::where(function ($query) use ($id) {
            $query->where('uuid', $id)->orWhere('public_id', $id);
        })
            ->where('company_uuid', session('company'))
            ->firstOrFail();
    }

    protected function findOrder(?string $id): Order
    {
        return Order::where(function ($query) use ($id) {
            $query->where('uuid', $id)->orWhere('public_id', $id);
        })
            ->where('company_uuid', session('company'))
            ->firstOrFail();
    }

    protected function findVehicle(?string $id): Vehicle
    {
        return Vehicle::where(function ($query) use ($id) {
            $query->where('uuid', $id)->orWhere('public_id', $id);
        })
            ->where('company_uuid', session('company'))
            ->firstOrFail();
    }

    /**
     * Update drivers geolocation data.
     *
     * @return \Illuminate\Http\Response
     */
    public function track(string $id, Request $request)
    {
        return app(ApiDriverController::class)->track($id, $request);
    }

    /**
     * Query for Storefront Customer orders.
     *
     * @return \Fleetbase\Http\Resources\Storefront\Customer
     */
    public function registerDevice(Request $request)
    {
        return app(ApiDriverController::class)->registerDevice($request);
    }

    /**
     * Authenticates customer using login credentials and returns with auth token.
     *
     * @return \Fleetbase\Http\Resources\Storefront\Customer
     */
    public function login(Request $request)
    {
        return app(ApiDriverController::class)->login($request);
    }

    /**
     * Attempts authentication with phone number via SMS verification.
     *
     * @return \Illuminate\Http\Response
     */
    public function loginWithPhone()
    {
        $phone = static::phone();

        // Check if user exists
        $user = User::where('phone', $phone)->whereNull('deleted_at')->withoutGlobalScopes()->first();

        if (!$user) {
            return response()->error('No driver with this phone # found.');
        }

        // Generate verification token
        VerificationCode::generateSmsVerificationFor($user, 'driver_login', [
            'messageCallback' => function ($verification) {
                return 'Your ' . config('app.name') . ' verification code is ' . $verification->code;
            },
        ]);

        return response()->json(['status' => 'OK']);
    }

    /**
     * Verifys SMS code and sends auth token with driver resource.
     *
     * @return \Fleetbase\Http\Resources\FleetOps\Driver
     */
    public function verifyCode(Request $request)
    {
        $identity = Utils::isEmail($request->identity) ? $request->identity : static::phone($request->identity);
        $code     = $request->input('code');
        $for      = $request->input('for', 'driver_login');
        $attrs    = $request->input(['name', 'phone', 'email']);

        if ($for === 'create_driver') {
            return app(ApiDriverController::class)->create($request);
        }

        // Check if user exists
        $user = User::where('phone', $identity)->orWhere('email', $identity)->first();
        if (!$user) {
            return response()->error('Unable to verify code.');
        }

        // Find and verify code
        $verificationCode = VerificationCode::where(['subject_uuid' => $user->uuid, 'code' => $code, 'for' => $for])->exists();
        if (!$verificationCode && $code !== config('fleetops.navigator.bypass_verification_code')) {
            return response()->error('Invalid verification code!');
        }

        // Get driver record
        $driver = Driver::where('user_uuid', $user->uuid)->whereNull('deleted_at')->withoutGlobalScopes()->first();
        if (!$driver) {
            return response()->error('No driver/agent record found for login.');
        }

        // Generate auth token
        try {
            $token = $user->createToken($driver->uuid);
        } catch (\Exception $e) {
            return response()->error($e->getMessage());
        }

        $driver->token = $token->plainTextToken;

        return new $this->resource($driver);
    }

    /**
     * Patches phone number with international code.
     */
    public static function phone(?string $phone = null): string
    {
        if ($phone === null) {
            $phone = request()->input('phone');
        }

        if (!Str::startsWith($phone, '+')) {
            $phone = '+' . $phone;
        }

        return $phone;
    }

    /**
     * Process import files (excel,csv) into Fleetbase order data.
     *
     * @return \Illuminate\Http\Response
     */
    public function import(ImportRequest $request)
    {
        $disk           = $request->input('disk', config('filesystems.default'));
        $files          = $request->resolveFilesFromIds();
        $importedCount  = 0;

        foreach ($files as $file) {
            try {
                $import = new DriverImport();
                Excel::import($import, $file->path, $disk);
                $importedCount += $import->imported;
            } catch (\Throwable $e) {
                return response()->error('Invalid file, unable to proccess.');
            }
        }

        return response()->json(['status' => 'ok', 'message' => 'Import completed', 'imported' => $importedCount]);
    }
}
