<?php
// Twilio webhook endpoint
// Flow: Ringba -> Twilio -> ZIP Resolver -> Ping SD -> Accept SD -> Dial SD buyer OR fallback to Ringba

$config = require __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../sd/ServiceDirectClient.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/shared_zip/zip_session.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/common_zip/lib/zip_helper.php';

header('Content-Type: text/xml; charset=UTF-8');

function twiml(string $xmlBody): void {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo "<Response>$xmlBody</Response>";
    exit;
}

function log_line(string $file, string $line): void {
    $ts = date('Y-m-d H:i:s');
    @file_put_contents($file, "[$ts] $line\n", FILE_APPEND);
}

function normalize_phone(?string $num): string {
    $num = trim((string)$num);
    if ($num === '') return '';

    $clean = preg_replace('/[^0-9\+]/', '', $num);

    if ($clean !== '' && $clean[0] === '+') {
        return $clean;
    }

    $digits = preg_replace('/\D/', '', $clean);

    if (strlen($digits) === 10) {
        return '+1' . $digits;
    }

    if (strlen($digits) === 11 && $digits[0] === '1') {
        return '+' . $digits;
    }

    return ($digits !== '') ? ('+' . $digits) : '';
}

function sd_ping_available(?array $j): bool {
    if (!is_array($j)) return false;
    return (bool)($j['data']['available_buyer'] ?? false);
}

function sd_ping_request_id(?array $j): ?int {
    if (!is_array($j)) return null;
    $rid = $j['data']['request_id'] ?? null;
    if (is_int($rid)) return $rid;
    if (is_string($rid) && ctype_digit($rid)) return (int)$rid;
    return null;
}

function sd_accept_phone(?array $j): ?string {
    if (!is_array($j)) return null;
    $p = $j['data']['phone_number'] ?? null;
    if (!is_string($p) || trim($p) === '') return null;
    return normalize_phone($p);
}

function load_bid_rules(array $config): array {
    $file = __DIR__ . '/../logviewer/bid-control/bids.json';
    $saved = [];

    if (is_file($file)) {
        $json = json_decode((string)file_get_contents($file), true);
        if (is_array($json)) {
            $saved = $json;
        }
    }

    $out = [];
    foreach (($config['verticals'] ?? []) as $vertical => $info) {
        $row = $saved[$vertical] ?? [];
        $out[$vertical] = [
            'enabled' => array_key_exists('enabled', $row) ? (bool)$row['enabled'] : true,
            'min_bid' => isset($row['min_bid']) && is_numeric($row['min_bid']) ? (float)$row['min_bid'] : 0.0,
        ];
    }
    return $out;
}

function bid_rule_for_vertical(string $vertical, array $config): array {
    $rules = load_bid_rules($config);
    return $rules[$vertical] ?? ['enabled' => true, 'min_bid' => 0.0];
}

function fallbackDial(array $vertCfg, int $dialTimeout): string {
    $fallback = normalize_phone($vertCfg['fallback_ringba_number'] ?? '');
    if ($fallback === '') {
        return "<Hangup/>";
    }
    $fbEsc = htmlspecialchars($fallback, ENT_QUOTES, 'UTF-8');
    return "<Dial timeout=\"{$dialTimeout}\">{$fbEsc}</Dial>";
}

function sd_attempt_ping(
    ServiceDirectClient $sd,
    array $config,
    array $vertCfg,
    string $vertical,
    string $zip,
    string $from,
    string $callSid,
    string $zipSource,
    string $twilioZip
): array {
    $source = $config['traffic_source'];
    $service_category = $vertCfg['service_category'];
    $isTest = ($config['environment'] === 'test');

    $pingPayload = [
        'sd_api_key' => $config['servicedirect']['api_key'],
        'zip_code' => $zip,
        'service_category' => $service_category,
        'source' => $source,
        'caller_id' => $from,
        'caller_id_in_post' => false,
        'test_mode' => $isTest,
        'zip_source' => $zipSource,
        'twilio_zip' => $twilioZip,
    ];

    if (!empty($config['servicedirect']['suppress_response_codes'])) {
        $pingPayload['suppress_response_codes'] = true;
    }

    $ping = $sd->ping($pingPayload);

    log_line(
        $config['logs']['sd'],
        "PING callSid={$callSid} zip={$zip} zip_source={$zipSource} twilio_zip={$twilioZip} payload=" . json_encode($pingPayload) . " resp=" . json_encode($ping)
    );

    return $ping;
}

function sd_self_action_url(string $vertical, array $extra = []): string {
    $query = array_merge([
        'step' => 'lookup',
        'vertical' => $vertical,
    ], $extra);

    return 'voice.php?' . http_build_query($query);
}

$vertical = $_GET['vertical'] ?? '';
$vertical = is_string($vertical) ? trim($vertical) : '';

if ($vertical === '' || !isset($config['verticals'][$vertical])) {
    log_line($config['logs']['twilio'], "ERROR: invalid vertical. query=" . json_encode($_GET));
    twiml('<Say>Configuration error. Please try again later.</Say><Hangup/>');
}

$vertCfg = $config['verticals'][$vertical];

$step = $_GET['step'] ?? 'gather';
$step = is_string($step) ? $step : 'gather';

$ivrEmpty = isset($_GET['ivr_empty']) ? (int)$_GET['ivr_empty'] : 0;

$from = normalize_phone($_POST['From'] ?? ($_REQUEST['From'] ?? ''));
$to   = normalize_phone($_POST['To'] ?? ($_REQUEST['To'] ?? ''));
$callSid = (string)($_POST['CallSid'] ?? ($_REQUEST['CallSid'] ?? ''));

$twilioZip = getTwilioZip($_REQUEST);
$zip_source = 'UNKNOWN';
$savedZip = '';

if (sz_enabled('servicedirect') && $step === 'gather') {
    $savedZip = sz_get($_REQUEST, 'servicedirect');

    if (!empty($savedZip)) {
        $_POST['Digits'] = $savedZip;
        $_REQUEST['Digits'] = $savedZip;
        $step = 'lookup';
        $zip_source = 'CACHED';

        log_line(
            $config['logs']['twilio'],
            "ZIP_REUSED callSid={$callSid} vertical={$vertical} zip={$savedZip} zip_source={$zip_source} twilio_zip={$twilioZip}"
        );
    }
}

if ($step === 'lookup' && $ivrEmpty === 1 && empty($_POST['Digits'])) {
    $zipResolved = resolveZipAfterIvr($_REQUEST, '');

    if (!empty($zipResolved['zip'])) {
        $_POST['Digits'] = $zipResolved['zip'];
        $_REQUEST['Digits'] = $zipResolved['zip'];
        $zip_source = $zipResolved['source'] ?? 'TWILIO_FALLBACK';

        log_line(
            $config['logs']['twilio'],
            "IVR_TIMEOUT_TWILIO_FALLBACK callSid={$callSid} vertical={$vertical} zip={$zipResolved['zip']} zip_source={$zip_source} twilio_zip={$twilioZip}"
        );

        logZipEvent("SD_IVR_TIMEOUT_TWILIO_FALLBACK | vertical={$vertical} | zip={$zipResolved['zip']} | source={$zip_source} | caller={$from} | callSid={$callSid}");
    }
}

if ($step === 'gather' && empty($_REQUEST['Digits'])) {
    $zipDecision = resolveInitialZip($_REQUEST);

    if (($zipDecision['action'] ?? '') === 'USE_ZIP' && !empty($zipDecision['zip'])) {
        $_POST['Digits'] = $zipDecision['zip'];
        $_REQUEST['Digits'] = $zipDecision['zip'];
        $step = 'lookup';
        $zip_source = $zipDecision['source'] ?? 'TWILIO';

        log_line(
            $config['logs']['twilio'],
            "ZIP_SWITCH_DIRECT callSid={$callSid} vertical={$vertical} zip={$zipDecision['zip']} zip_source={$zip_source} twilio_zip={$twilioZip}"
        );
    }
}

log_line(
    $config['logs']['twilio'],
    "INCOMING callSid={$callSid} vertical={$vertical} from={$from} to={$to} step={$step} ivr_empty={$ivrEmpty} twilio_zip={$twilioZip} cached_zip={$savedZip} zip_source={$zip_source}"
);

$voice = htmlspecialchars($config['twilio']['voice']['voice'], ENT_QUOTES, 'UTF-8');
$lang  = htmlspecialchars($config['twilio']['voice']['language'], ENT_QUOTES, 'UTF-8');
$gatherTimeout = (int)$config['twilio']['timeouts']['gather'];
$dialTimeout   = (int)$config['twilio']['timeouts']['dial'];

if ($step === 'gather') {
    $action = htmlspecialchars(sd_self_action_url($vertical, ['ivr_empty' => 1]), ENT_QUOTES, 'UTF-8');

    $xml = ''
        . "<Gather input=\"dtmf\" numDigits=\"5\" timeout=\"{$gatherTimeout}\" action=\"{$action}\" method=\"POST\" actionOnEmptyResult=\"true\">"
        . "<Say voice=\"{$voice}\" language=\"{$lang}\">Please enter your 5 digit zip code.</Say>"
        . "</Gather>"
        . "<Redirect method=\"POST\">{$action}</Redirect>";

    twiml($xml);
}

if ($step === 'lookup') {
    $rawDigits = (string)($_POST['Digits'] ?? '');
    $digits = preg_replace('/\D/', '', $rawDigits);

    if ($zip_source === 'UNKNOWN') {
        $zip_source = 'IVR';
    }

    if (strlen($digits) !== 5) {
        $zipResolved = resolveZipAfterIvr($_REQUEST, $rawDigits);

        if (!empty($zipResolved['zip'])) {
            $digits = $zipResolved['zip'];
            $zip_source = $zipResolved['source'] ?? 'TWILIO_FALLBACK';

            $_POST['Digits'] = $digits;
            $_REQUEST['Digits'] = $digits;

            log_line(
                $config['logs']['twilio'],
                "ZIP_FALLBACK_USED callSid={$callSid} vertical={$vertical} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip}"
            );
        } else {
            log_line(
                $config['logs']['twilio'],
                "ZIP_INVALID callSid={$callSid} digits={$digits} twilio_zip={$twilioZip} zip_source=FAILED"
            );

            $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">That zip code seems invalid.</Say>"
                 . fallbackDial($vertCfg, $dialTimeout);
            twiml($xml);
        }
    }

    if (sz_enabled('servicedirect')) {
        sz_store($_REQUEST, 'servicedirect', $digits);
    }

    logZipEvent("SD_SELECTED_ZIP | vertical={$vertical} | zip={$digits} | source={$zip_source} | twilio_zip={$twilioZip} | caller={$from} | callSid={$callSid}");

    $sd = new ServiceDirectClient($config);

    $service_category = $vertCfg['service_category'];
    $isTest = ($config['environment'] === 'test');

    $ping = sd_attempt_ping($sd, $config, $vertCfg, $vertical, $digits, $from, $callSid, $zip_source, $twilioZip);
    $pingJson = $ping['json'] ?? null;

    if (!$ping['ok'] || !sd_ping_available($pingJson)) {
        log_line(
            $config['logs']['sd'],
            "PING_REJECT callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} vertical={$vertical} http={$ping['http_code']}"
        );

        $retryDecision = shouldRetryWithTwilioZip($_REQUEST, $digits, $zip_source);

        if ($retryDecision && !empty($retryDecision['zip'])) {
            $originalZip = $digits;
            $retryZip = $retryDecision['zip'];
            $retrySource = $retryDecision['source'] ?? 'TWILIO_RETRY';

            logZipEvent("SD_NO_COVERAGE_RETRY | original_zip={$originalZip} | original_source={$zip_source} | retry_zip={$retryZip} | caller={$from} | callSid={$callSid}");

            $retryPing = sd_attempt_ping($sd, $config, $vertCfg, $vertical, $retryZip, $from, $callSid, $retrySource, $twilioZip);
            $retryPingJson = $retryPing['json'] ?? null;

            if ($retryPing['ok'] && sd_ping_available($retryPingJson)) {
                $digits = $retryZip;
                $zip_source = 'TWILIO_RETRY_SUCCESS';
                $ping = $retryPing;
                $pingJson = $retryPingJson;

                $_POST['Digits'] = $digits;
                $_REQUEST['Digits'] = $digits;

                if (sz_enabled('servicedirect')) {
                    sz_store($_REQUEST, 'servicedirect', $digits);
                }

                log_line(
                    $config['logs']['sd'],
                    "TWILIO_ZIP_RETRY_SUCCESS callSid={$callSid} original_zip={$originalZip} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} vertical={$vertical} http={$ping['http_code']}"
                );

                logZipEvent("SD_RETRY_SUCCESS | zip={$digits} | source={$zip_source} | caller={$from} | callSid={$callSid}");
            } else {
                log_line(
                    $config['logs']['sd'],
                    "TWILIO_ZIP_RETRY_FAILED callSid={$callSid} original_zip={$originalZip} retry_zip={$retryZip} zip_source=TWILIO_RETRY_FAILED twilio_zip={$twilioZip} vertical={$vertical} http={$retryPing['http_code']}"
                );

                logZipEvent("SD_RETRY_FAILED | retry_zip={$retryZip} | caller={$from} | callSid={$callSid}");

                $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>"
                     . fallbackDial($vertCfg, $dialTimeout);
                twiml($xml);
            }
        } else {
            $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>"
                 . fallbackDial($vertCfg, $dialTimeout);
            twiml($xml);
        }
    }

    $bid = (float)($pingJson['data']['bid'] ?? 0);
    $buyerId = (string)($pingJson['data']['buyer_id'] ?? '');
    $requestId = sd_ping_request_id($pingJson);

    $rule = bid_rule_for_vertical($vertical, $config);
    $thresholdEnabled = (bool)$rule['enabled'];
    $thresholdBid = (float)$rule['min_bid'];

    if ($thresholdEnabled && $thresholdBid > 0 && $bid < $thresholdBid) {
        log_line(
            $config['logs']['sd'],
            "LOW_BID_REJECT callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} vertical={$vertical} service_category={$service_category} bid={$bid} threshold={$thresholdBid} buyer_id={$buyerId} request_id={$requestId} http={$ping['http_code']}"
        );
        log_line(
            $config['logs']['twilio'],
            "LOW_BID_REJECT callSid={$callSid} vertical={$vertical} from={$from} bid={$bid} threshold={$thresholdBid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip}"
        );

        $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>"
             . fallbackDial($vertCfg, $dialTimeout);
        twiml($xml);
    }

    if ($requestId === null) {
        log_line(
            $config['logs']['sd'],
            "PING_NO_REQUEST_ID callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} resp=" . json_encode($pingJson)
        );

        $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>"
             . fallbackDial($vertCfg, $dialTimeout);
        twiml($xml);
    }

    $acceptPayload = [
        'sd_api_key' => $config['servicedirect']['api_key'],
        'test_mode'  => $isTest,
    ];

    if (!empty($config['servicedirect']['suppress_response_codes'])) {
        $acceptPayload['suppress_response_codes'] = true;
    }

    $accept = $sd->accept($requestId, $acceptPayload);

    log_line(
        $config['logs']['sd'],
        "ACCEPT callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} request_id={$requestId} payload=" . json_encode($acceptPayload) . " resp=" . json_encode($accept)
    );

    $dest = sd_accept_phone($accept['json'] ?? null);

    if (!$accept['ok'] || !$dest) {
        log_line(
            $config['logs']['sd'],
            "ACCEPT_REJECT callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} request_id={$requestId} http={$accept['http_code']}"
        );

        $xml = "<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>"
             . fallbackDial($vertCfg, $dialTimeout);
        twiml($xml);
    }

    log_line(
        $config['logs']['sd'],
        "DIAL callSid={$callSid} zip={$digits} zip_source={$zip_source} twilio_zip={$twilioZip} vertical={$vertical} bid={$bid} buyer_id={$buyerId} request_id={$requestId} dest={$dest}"
    );

    $destEsc = htmlspecialchars($dest, ENT_QUOTES, 'UTF-8');

    $xml = ""
        . "<Say voice=\"{$voice}\" language=\"{$lang}\">Connecting you now.</Say>"
        . "<Dial timeout=\"{$dialTimeout}\">{$destEsc}</Dial>";

    twiml($xml);
}

twiml("<Say voice=\"{$voice}\" language=\"{$lang}\">Please hold while we connect you.</Say>" . fallbackDial($vertCfg, $dialTimeout));