<?php
if ( ! defined( 'ABSPATH' ) ) exit;
use Twilio\Jwt\AccessToken;
use Twilio\Jwt\Grants\VoiceGrant;
use Twilio\TwiML\VoiceResponse;

class DialBunnyWebCallingController
{
    // Get token to enable browser calling
    function browser_token() {
      // Permission + authentication
      if ( !isset($_POST['user_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['user_nonce'])), 'DialBunny') ) { wp_send_json_error('Permission denied', 403); exit; }
      if ( ! current_user_can('read') ) {
        wp_send_json_error('Permission denied', 403);
        exit;
      }

      $user_id = get_current_user_id();
      $user_login = wp_get_current_user()->user_login;
      global $wpdb;
      $table = $wpdb->prefix . 'dialbunny_phone_details';

      // Try to get existing token within the last 15 minutes
      // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
      $result = $wpdb->get_row($wpdb->prepare("SELECT web_token FROM {$wpdb->prefix}dialbunny_phone_details WHERE user = %d AND token_created >= (NOW() - INTERVAL 15 MINUTE) LIMIT 1",$user_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
      // AND token_created >= (NOW() - INTERVAL 15 MINUTE)

      if (isset($opened_item[0]->web_token)) {
        echo wp_json_encode([
          'identity' => $user_login,
          'token'    => $result->web_token
        ]);
        exit;
      }

      // Check if user has a managed number
      // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
      $number_row = $wpdb->get_row($wpdb->prepare("SELECT managed_number FROM {$wpdb->prefix}dialbunny_phone_details WHERE user = %d LIMIT 1",$user_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

      if (! $number_row || empty($number_row->managed_number)) {
        wp_send_json_error('No managed number found for user', 404);
        exit;
      }

      // Load API keys
      $keys = new DialBunnyKeysClass();
      $sid        = $keys->TWILIO_SID();
      $appSid     = $keys->TwiML_App_SID();
      $apiKey     = $keys->API_KEY();
      $apiSecret  = $keys->API_SECRET();

      // Generate token
      $token = new AccessToken($sid, $apiKey, $apiSecret, 3600, $user_login);
      $voiceGrant = new VoiceGrant();
      $voiceGrant->setOutgoingApplicationSid($appSid);
      $voiceGrant->setIncomingAllow(true);
      $token->addGrant($voiceGrant);
      $jwt = $token->toJWT();

      // Save token
      // Direct database query used below – Safely updates data into the database using $wpdb->update(), which handles proper escaping and prevents SQL injection.
      $wpdb->update($table, ['web_token' => $jwt,'token_created' => current_time('mysql', 1)], ['user' => $user_id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

      echo wp_json_encode([
        'identity' => $user_login,
        'token'    => $jwt
      ]);
      exit;
  }

  // Setup Browser voice call
  function calls_logic()
  {
    if ( !isset($_POST['user_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['user_nonce'])), 'DialBunny') ) {  }
    global $wpdb;

    // --- 1. Web browser outgoing call ---
    if (isset($_POST['BrowserOutgoing']) && $_POST['BrowserOutgoing'] === 'true')
    {
      if (!isset($_POST['To_number'])) { exit; }
      if (!isset($_POST['To_code'])) { exit; }
      $response = new VoiceResponse();
      $user_id = filter_input(INPUT_POST, 'User', FILTER_VALIDATE_INT); if ($user_id === false || $user_id === null) { exit; }

      $table = $wpdb->prefix . 'dialbunny_phone_details';
      // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
      $details = $wpdb->get_row($wpdb->prepare("SELECT managed_number_code, managed_number FROM {$wpdb->prefix}dialbunny_phone_details WHERE user = %d LIMIT 1", $user_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
      if (! $details) { exit; }
      $number_countrycode = sanitize_text_field(wp_unslash($_POST['To_code']));
      $number = sanitize_text_field(wp_unslash($_POST['To_number']));
      $dial = $response->dial('', ['callerId' => $details->managed_number_code . $details->managed_number]);
      if (preg_match("/^[\d\+\-\(\) ]+$/", $number_countrycode.$number)) {
        $dial->number($number_countrycode.$number);
      } else {
        $dial->client($number);
      }

      $wpdb->insert($wpdb->prefix . 'dialbunny_recents', [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            'id' => uniqid(),
            'callsid' => 'outbound',
            'from_countrycode' => $details->managed_number_code,
            'from_number' => $details->managed_number,
            'to_countrycode' => $number_countrycode,
            'to_number' => $number,
            'created_at' => gmdate("Y-m-d H:i:s")
        ]);

      // Twilio expects raw XML, Twilio will fail to parse this response if escaped
      echo $response; // phpcs:ignore WordPress.Security.EscapeOutput
      exit;
    }
    // --- 2. Incoming call ---
    $table = $wpdb->prefix . 'dialbunny_phone_details';
    $contacts = $wpdb->prefix . 'dialbunny_contacts';

    // Format and separate the phone number from the country code
    $fromNumber = '';
    $fromCountryCode = '';
    $toNumber = '';
    $toCountryCode = '';

    if (isset($_POST['From'])) {
      $rawFrom = sanitize_text_field(wp_unslash($_POST['From']));
      $fromNumber = preg_replace('/\+(?:998|996|995|994|993|992|977|976|975|974|973|972|971|970|968|967|966|965|964|963|962|961|960|886|880|856|855|853|852|850|692|691|690|689|688|687|686|685|683|682|681|680|679|678|677|676|675|674|673|672|670|599|598|597|595|593|592|591|590|509|508|507|506|505|504|503|502|501|500|423|421|420|389|387|386|385|383|382|381|380|379|378|377|376|375|374|373|372|371|370|359|358|357|356|355|354|353|352|351|350|299|298|297|291|290|269|268|267|266|265|264|263|262|261|260|258|257|256|255|254|253|252|251|250|249|248|246|245|244|243|242|241|240|239|238|237|236|235|234|233|232|231|230|229|228|227|226|225|224|223|222|221|220|218|216|213|212|211|98|95|94|93|92|91|90|86|84|82|81|66|65|64|63|62|61|60|58|57|56|55|54|53|52|51|49|48|47|46|45|44\D?1624|44\D?1534|44\D?1481|44|43|41|40|39|36|34|33|32|31|30|27|20|7|1\D?939|1\D?876|1\D?869|1\D?868|1\D?849|1\D?829|1\D?809|1\D?787|1\D?784|1\D?767|1\D?758|1\D?721|1\D?684|1\D?671|1\D?670|1\D?664|1\D?649|1\D?473|1\D?441|1\D?345|1\D?340|1\D?284|1\D?268|1\D?264|1\D?246|1\D?242|1)\D?/', '', $rawFrom);
      $fromCountryCode = substr($rawFrom, 0, -strlen($fromNumber));
    }

    if (isset($_POST['To'])) {
      $rawTo = sanitize_text_field(wp_unslash($_POST['To']));
      $toNumber = preg_replace('/\+(?:998|996|995|994|993|992|977|976|975|974|973|972|971|970|968|967|966|965|964|963|962|961|960|886|880|856|855|853|852|850|692|691|690|689|688|687|686|685|683|682|681|680|679|678|677|676|675|674|673|672|670|599|598|597|595|593|592|591|590|509|508|507|506|505|504|503|502|501|500|423|421|420|389|387|386|385|383|382|381|380|379|378|377|376|375|374|373|372|371|370|359|358|357|356|355|354|353|352|351|350|299|298|297|291|290|269|268|267|266|265|264|263|262|261|260|258|257|256|255|254|253|252|251|250|249|248|246|245|244|243|242|241|240|239|238|237|236|235|234|233|232|231|230|229|228|227|226|225|224|223|222|221|220|218|216|213|212|211|98|95|94|93|92|91|90|86|84|82|81|66|65|64|63|62|61|60|58|57|56|55|54|53|52|51|49|48|47|46|45|44\D?1624|44\D?1534|44\D?1481|44|43|41|40|39|36|34|33|32|31|30|27|20|7|1\D?939|1\D?876|1\D?869|1\D?868|1\D?849|1\D?829|1\D?809|1\D?787|1\D?784|1\D?767|1\D?758|1\D?721|1\D?684|1\D?671|1\D?670|1\D?664|1\D?649|1\D?473|1\D?441|1\D?345|1\D?340|1\D?284|1\D?268|1\D?264|1\D?246|1\D?242|1)\D?/', '', $rawTo);
      $toCountryCode = substr($rawTo, 0, -strlen($toNumber));
    }

    // Now safely use $toNumber and $toCountryCode in $wpdb->prepare:
    // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
    $opened_item = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}dialbunny_phone_details WHERE managed_number_code = %s AND managed_number = %s LIMIT 1",$toCountryCode,$toNumber)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    // Voice assistant data
    $voiceAssistantData = json_decode($opened_item[0]->voice_assistant, true);
    // User data
    // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
    $userData = $wpdb->get_results($wpdb->prepare("SELECT user_login FROM {$wpdb->prefix}users WHERE ID = %d LIMIT 1",intval($opened_item[0]->user))); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    // build normalized cleaned number for comparison
    $fromNumberClean = preg_replace('/[\W]/', '', $fromCountryCode . $fromNumber);
    // Check to see if phone number exists in database and if not reject the call
    if (!isset($opened_item[0]->user)) { echo '<Response><Reject reason="busy" /></Response>'; return; }

    // verifying numbers
    $callNumbers = ['call1', 'call2', 'call3'];
    foreach ($callNumbers as $index => $callField) {
        $verifiedField = $callField . '_verified';
        if ($opened_item[0]->$verifiedField == 0 && $fromNumberClean === preg_replace('/[\W]/', '', $opened_item[0]->$callField)) {
            $data = [$verifiedField => 1];
            // Direct database query used below – Safely updates data into the database using $wpdb->update(), which handles proper escaping and prevents SQL injection.
            $wpdb->update($table, $data, ['managed_number_code' => $toCountryCode,'managed_number' => $toNumber]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            echo '<Response><Say>Your phone number ' . esc_html($index + 1) . ' has been verified</Say></Response>';
            return;
        }
    }

    // Already verified message
    foreach ($callNumbers as $callField) {
        if ($fromNumberClean === preg_replace('/[\W]/', '', $opened_item[0]->$callField)) {
            echo '<Response><Say>The phone number you are calling from has already been verified</Say></Response>';
            return;
        }
    }

    // Block caller if they are blocked from contacts
    // Direct database query used below – $wpdb->prepare() is always used to prevent SQL injection.
    $blocked = $wpdb->get_results($wpdb->prepare("SELECT blocked FROM {$wpdb->prefix}dialbunny_contacts WHERE phone = %s AND user = %d LIMIT 1",$fromNumber,$opened_item[0]->user)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (isset($blocked[0]->blocked) && $blocked[0]->blocked == 1) { echo '<Response><Reject reason="busy" /></Response>'; return; }

    // check to see whether the caller is calling within the set business hours
    $withinHours = true; // TRUE meaning the caller is calling within the set hours

    if ($opened_item[0]->business_hours != null && $opened_item[0]->timezone != null)
    {
      try {
        $timezone = new DateTimeZone($opened_item[0]->timezone);
        $dateTime = new DateTime('now', $timezone);
      } catch (Exception $e) {
        // Invalid timezone
        $withinHours = false;
        return;
      }
      $jsonData = json_decode($opened_item[0]->business_hours);
      $withinHours = true; // default to true and mark false as needed
      if ($dateTime->format('N') == 1)
      {
        if (!empty($jsonData->monday_status) && $jsonData->monday_status == 1)
        {
          if (!empty($jsonData->monday1) && !empty($jsonData->monday2))
          {
            if (preg_replace("/[^0-9]/", "", $jsonData->monday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->monday2)) {  } else { $withinHours = false; }
          }
        }
        else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 2)
        {
          if (!empty($jsonData->tuesday_status) && $jsonData->tuesday_status == 1)
          {
            if (!empty($jsonData->tuesday1) && !empty($jsonData->tuesday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->tuesday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->tuesday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 3)
        {
          if (!empty($jsonData->wednesday_status) && $jsonData->wednesday_status == 1)
          {
            if (!empty($jsonData->wednesday1) && !empty($jsonData->wednesday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->wednesday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->wednesday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 4)
        {
          if (!empty($jsonData->thursday_status) && $jsonData->thursday_status == 1)
          {
            if (!empty($jsonData->thursday1) && !empty($jsonData->thursday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->thursday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->thursday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 5)
        {
          if (!empty($jsonData->friday_status) && $jsonData->friday_status == 1)
          {
            if (!empty($jsonData->friday1) && !empty($jsonData->friday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->friday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->friday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 6)
        {
          if (!empty($jsonData->saturday_status) && $jsonData->saturday_status == 1)
          {
            if (!empty($jsonData->saturday1) && !empty($jsonData->saturday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->saturday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->saturday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
        if ($dateTime->format('N') == 7)
        {
          if (!empty($jsonData->sunday_status) && $jsonData->sunday_status == 1)
          {
            if (!empty($jsonData->sunday1) && !empty($jsonData->sunday2))
            {
              if (preg_replace("/[^0-9]/", "", $jsonData->sunday1) <= $dateTime->format('Hi') && $dateTime->format('Hi') <= preg_replace("/[^0-9]/", "", $jsonData->sunday2)) {  } else { $withinHours = false; }
            }
          }
          else { $withinHours = false; }
        }
      }

    // check the users set business hours and then connect the call
    if (!isset($_POST['CallSid'])) { echo '<Response><Reject reason="busy" /></Response>'; exit; }
    $callsid = sanitize_text_field(wp_unslash($_POST['CallSid'])); if ( ! preg_match( '/^CA[a-f0-9]{32}$/i', $callsid ) ) { echo '<Response><Reject reason="busy" /></Response>'; exit; }
    $response = new \Twilio\TwiML\VoiceResponse();

    if (!$withinHours) {
        // Outside hours
        $sayText = $opened_item[0]->outside_hours_message ?: 'You have called outside of business hours, please call again later.';
        $response->say($sayText);
        // AI assistant response
        if ($voiceAssistantData['ai_timetable'] == 1)
        {
          if ($voiceAssistantData['voice_company'] === 'twilio') {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['twilio_sid'])) { $response->say("No valid Twilio SID key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $connect = $response->connect();
            $connect->assistant([
                'id' => $voiceAssistantData['twilio_sid'],
                'welcomeGreeting' => $voiceAssistantData['greeting'],
                'voice' => $voiceAssistantData['voice'],
            ]);
          }
          elseif ($voiceAssistantData['voice_company'] === 'vapi')
          {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['vapi_sip'])) { $response->say("No valid Vapi SIP key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $response->dial()->sip($voiceAssistantData['vapi_sip']);
          }
        }
        // Twilio expects raw XML, Twilio will fail to parse this response if escaped
        echo $response; // phpcs:ignore WordPress.Security.EscapeOutput
        // Direct database query used below – Safely inserts data into the database using $wpdb->insert(), which handles proper escaping and prevents SQL injection.
        $wpdb->insert($wpdb->prefix . 'dialbunny_recents', [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            'id' => uniqid(),
            'callsid' => $callsid,
            'from_countrycode' => $fromCountryCode,
            'from_number' => $fromNumber,
            'to_countrycode' => $toCountryCode,
            'to_number' => $toNumber,
            'created_at' => gmdate("Y-m-d H:i:s")
        ]);
      }
      else {
        if (!empty($opened_item[0]->welcome)) {
          // Add welcome message if any before connecting the call
          $response->say($opened_item[0]->welcome);
        }

        // AI assistant response
        if ($voiceAssistantData['ai_initiated'] == 1)
        {
          if ($voiceAssistantData['voice_company'] === 'twilio') {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['twilio_sid'])) { $response->say("No valid Twilio SID key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $connect = $response->connect();
            $connect->assistant([
                'id' => $voiceAssistantData['twilio_sid'],
                'welcomeGreeting' => $voiceAssistantData['greeting'],
                'voice' => $voiceAssistantData['voice'],
            ]);
          }
          elseif ($voiceAssistantData['voice_company'] === 'vapi')
          {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['vapi_sip'])) { $response->say("No valid Vapi SIP key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $response->dial()->sip($voiceAssistantData['vapi_sip']);
          }
        }

        // Connect incoming call
        $dial = $response->dial();
        $dial->client($userData[0]->user_login ?? 'unknown'); // fallback just in case
        if (strlen($opened_item[0]->call1) > 4) {$dial->number($opened_item[0]->call1);}
        if (strlen($opened_item[0]->call2) > 4) {$dial->number($opened_item[0]->call2);}
        if (strlen($opened_item[0]->call3) > 4) {$dial->number($opened_item[0]->call3);}

        if (!empty($opened_item[0]->busy)) {
            $response->say($opened_item[0]->busy);
        } else {
            $response->say('Sorry, there are currently no available representatives. Good Bye.');
        }

        // AI assistant response
        if ($voiceAssistantData['ai_busy'] == 1)
        {
          if ($voiceAssistantData['voice_company'] === 'twilio') {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['twilio_sid'])) { $response->say("No valid Twilio SID key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $connect = $response->connect();
            $connect->assistant([
                'id' => $voiceAssistantData['twilio_sid'],
                'welcomeGreeting' => $voiceAssistantData['greeting'],
                'voice' => $voiceAssistantData['voice'],
            ]);
          }
          elseif ($voiceAssistantData['voice_company'] === 'vapi')
          {
            // Twilio expects raw XML, Twilio will fail to parse this response if escaped
            if (empty($voiceAssistantData['vapi_sip'])) { $response->say("No valid Vapi SIP key was saved. Goodbye."); echo $response; exit; } // phpcs:ignore WordPress.Security.EscapeOutput
            $response->dial()->sip($voiceAssistantData['vapi_sip']);
          }
        }

        // Twilio expects raw XML, Twilio will fail to parse this response if escaped
        echo $response; // phpcs:ignore WordPress.Security.EscapeOutput
        // Direct database query used below – Safely inserts data into the database using $wpdb->insert(), which handles proper escaping and prevents SQL injection.
        $wpdb->insert($wpdb->prefix . 'dialbunny_recents', [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            'id' => uniqid(),
            'callsid' => $callsid,
            'from_countrycode' => $fromCountryCode,
            'from_number' => $fromNumber,
            'to_countrycode' => $toCountryCode,
            'to_number' => $toNumber,
            'created_at' => gmdate("Y-m-d H:i:s")
        ]);
    }

  }
}
