<?php

namespace App\Services\Security\Crowdstrike;

use App\Models\Asset;
use App\Repositories\ApiCredentialRepository;
use App\Services\Integrations\Security\Crowdstrike\CrowdstrikeIntegration;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class CrowdstrikeApiService
{

    private $slug;
    private $name;
    private $apiCredentialRepo;
    private $integration;

    /**
     * Constructor for CrowdstrikeService
     * 
     * @param ApiCredentialRepository $apiCredentialRepo
     * @param CrowdstrikeIntegration $integration
     */
    public function __construct(ApiCredentialRepository $apiCredentialRepo, CrowdstrikeIntegration $integration)
    {
        $this->slug = 'crowdstrike';
        $this->name = 'Crowdstrike';
        $this->apiCredentialRepo = $apiCredentialRepo;
        $this->integration = $integration;
    }


    /**
     * get crowdstrike api credentials
     * 
     * @return mixed
     */
    public function getCredentials()
    {
        $data = Cache::get('crowdstrikeCredentials');

        if ($data === null) {
            $data =  $this->apiCredentialRepo->getCredentials($this->slug);
            Cache::put('crowdstrikeCredentials', $data);
        }

        return $data;
    }

    /**
     * add credentials for crowdstrike
     * 
     * @param array $credentialsData
     * 
     * @return bool
     */
    public function addCredentials($credentialsData)
    {
        try {
            $this->apiCredentialRepo->addCredentials($credentialsData, $this->slug, $this->name);

            return true;
        } catch (Exception $e) {
            Log::channel('daily')->error($this->name . ' credential create : ' . $e->getMessage());

            return false;
        }
    }

    /**
     * update credentials for crowdstrike
     * 
     * @param array $data
     * 
     * @return bool
     */
    public function updateCredentials($data)
    {
        try {
            $credentialsData = $this->getCredentials();

            if (!$credentialsData) {
                return false;
            }
            $updateData = [
                'key'       => $data['app_key'] ? $data['app_key'] : $credentialsData->key,
                'password'  => $data['app_secret'] ? $data['app_secret'] : $credentialsData->password
            ];
            $this->apiCredentialRepo->updateCredentials($updateData, $this->slug);
            Cache::forget('crowdstrikeCredentials');

            return true;
        } catch (Exception $e) {
            Log::channel('daily')->error($this->name . ' credential create : ' . $e->getMessage());

            return false;
        }
    }

    /**
     * check status of the crowdstrike api connection
     * 
     * 
     * @return bool
     */
    public function checkConnection()
    {
        $generatedAccessTokenResponse = $this->getAccessToken();

        if (!$generatedAccessTokenResponse) {
            return false;
        }

        return true;
    }



    /**
     * fetch access token from crowdstrike api credentials data
     * 
     * 
     * @return mixed
     */
    public function getAccessToken()
    {

        $credentialsData = $this->getCredentials();
        $generatedAccessTokenResponse = $this->integration->generateAccessToken($credentialsData->key, $credentialsData->password);

        if (!$generatedAccessTokenResponse) {
            return false;
        }

        return $generatedAccessTokenResponse['access_token'] ?? false;
    }

    /**
     * Syncing CrowdStrike ID to the assets if both serial numbers are matching.
     *
     * @return int 
     */
    public function syncCrowdstrikeIdToAssets()
    {
        $offset = 0;
        $updatedAssetsCount = 0;

        // adding crowdstrike id to the assets if both serial numbers are matching
        do {
            //we need to fetch the devices ids from crowdstrike for passing params to other endpoints 
            $devicesIdsApiResponse = $this->getDeviceIds($offset);

            if (empty($devicesIdsApiResponse)) {
                break;
            }
            $deviceIds = $devicesIdsApiResponse['resources'] ?? [];
            $offset = $devicesIdsApiResponse['meta']['pagination']['offset'];

            //save an id from crowdstrike to our assets which matching the serial numbers 
            $saveCrowdStrikeDataToAssetsResponse = $this->saveCrowdStrikeData($deviceIds);
            $updatedAssetsCount +=  $saveCrowdStrikeDataToAssetsResponse;
        } while ($devicesIdsApiResponse['meta']['pagination']['total'] != $devicesIdsApiResponse['meta']['pagination']['offset']);

        return $updatedAssetsCount;
    }

    /**
     * Retrieves the device IDs from the integration API.
     *
     * @param int $offset The offset for pagination.
     * @param int $limit The maximum number of device IDs to retrieve.
     * 
     * @return array The array of device IDs.
     */
    public function getDeviceIds($offset)
    {
        $accessToken = $this->getAccessToken();

        return $this->integration->getDeviceIds($accessToken, $offset);
    }

    /**
     * Saves CrowdStrike data for the given device IDs.
     *
     * @param array $deviceIds The array of device IDs.
     * @return int The count of matching assets.
     */
    public function saveCrowdStrikeData($deviceIds)
    {
        $matchingAssetCount = 0;

        foreach ($deviceIds as $deviceId) {
            $deviceDetailsApiResponse = $this->getDeviceData($deviceId);
            $deviceData = $deviceDetailsApiResponse['resources'][0] ?? [];
            $assetSaveWithCrowdstrikeID = $this->assetSaveWithCrowdstrikeID($deviceData);

            if ($assetSaveWithCrowdstrikeID) {
                $matchingAssetCount++;
            }
        }

        return $matchingAssetCount;
    }

    /**
     * Save asset with CrowdStrike ID if serial # exists.
     *
     * @param array $deviceData The device data.
     * @return bool Returns false if the asset was not saved.
     */
    public function assetSaveWithCrowdstrikeID($deviceData)
    {
        if (!$deviceData['serial_number']) {
            return false;
        }
        $matchingAsset = Asset::where('serial_no', $deviceData['serial_number'])->select('id', 'crowdstrike_id')->first();


        if (!$matchingAsset) {
            return false;
        }

        if ($deviceData['device_id'] != $matchingAsset->crowdstrike_id) {
            $matchingAsset->update(['crowdstrike_id' => $deviceData['device_id']]);
        }

        return true;
    }

    /**
     * Retrieves the device data for a given device ID.
     *
     * @param int $deviceId The ID of the device.
     * @throws Some_Exception_Class A description of the exception that can be thrown.
     * @return array device details.
     */
    public function getDeviceData($deviceId)
    {
        $accessToken = $this->getAccessToken();

        return $this->integration->getDeviceData($accessToken, $deviceId);
    }

    /**
     * Retrieves vulnerabilities with a limit for a given device.
     *
     * @param mixed $deviceCrowdstrikeID The device for which to retrieve vulnerabilities.
     * @throws Exception If an error occurs while retrieving the vulnerability data.
     * @return array An array of vulnerabilities.
     */
    public function getVulnerbilitiesWithLimit($deviceCrowdstrikeID)
    {
        $paginationAfterToken = '';
        $vulnerabilities = [];

        do {
            $vulnerabilityData = $this->getVulnerabilityData($deviceCrowdstrikeID, $paginationAfterToken);

            if (empty($vulnerabilityData)) {
                break;
            }
            $vulnerabilities = array_merge($vulnerabilities, $vulnerabilityData['resources']);
            $paginationAfterToken = $vulnerabilityData['meta']['pagination']['after'];
        } while ($vulnerabilityData['meta']['pagination']['after'] != '');

        return $vulnerabilities;
    }


    /**
     * Retrieves vulnerability data for a device.
     *
     * @param mixed $deviceCrowdstrikeID device crowdstrike id.
     * @param string|null $paginationAfterToken The pagination token for retrieving the next set of vulnerability data.
     * @throws Some_Exception_Class A description of the exception that can be thrown.
     * @return mixed The vulnerability data for the device.
     */
    public function getVulnerabilityData($deviceCrowdstrikeID, $paginationAfterToken)
    {
        $accessToken = $this->getAccessToken();

        //Set up your filters
        $filters = [
            "aid:'" . $deviceCrowdstrikeID . "'",
            "status:'open'",
        ];
        // Combine filters with the + operator
        $filterString = implode('+', $filters);

        return $this->integration->getVulnerabilityData($accessToken, $filterString, $paginationAfterToken);
    }
}
