<?php
/**
 * ivr.php — Windshields To Go USA · Twilio TwiML IVR Order System
 * Upload to: /home/auto11/public_html/ivr/ivr.php
 *
 * Twilio webhook URL: https://autoglasshosting.com/ivr/ivr.php
 *
 * Flow: Welcome+ZIP → Year → Make → Model → Style → Glass
 *       → First → Last → Street → City → State → Phone → Email
 *       → PO/RO → Unit → Notes → Write order → SMS + Email VIN link
 *
 * Account routing by Twilio "To" param (original RC DNIS passed through):
 *   +18778072201 → PTL   (Penske)
 *   +18778457544 → LINDE
 *   +18772240909 → LINCARE
 *
 * STT: Deepgram Nova-2 via Twilio <Gather>
 * Press 9 at any time to start over
 * Press 0 at any time to transfer to live agent
 */

// ─── CONFIG ───────────────────────────────────────────────────────────────────
$config = require '/home/auto11/var/config.php';

define('TWILIO_ACCOUNT_SID', $config['twilio_account_sid'] ?? '');
define('TWILIO_API_KEY_SID', $config['twilio_api_key_sid'] ?? '');
define('TWILIO_API_SECRET',  $config['twilio_api_secret']  ?? '');
define('TWILIO_NUMBER',      $config['twilio_number']      ?? '');
define('DEEPGRAM_API_KEY',   $config['deepgram_api_key']   ?? '');

define('RC_CLIENT_ID',       'XH2IPXECRWdczFx0giHgxh');
define('RC_CLIENT_SECRET',   'bOEeTScZ7pwepvzW5tA9gvXa1pQNT1zRZbgSyNBWJap8');
define('RC_SERVER',          'https://platform.ringcentral.com');
define('RC_SMS_FROM',        '+18059785161');

define('DB_HOST',  'localhost');
define('DB_USER',  'windshi_quote');
define('DB_PASS',  $config['db_password'] ?? '');
define('DB_NAME',  'windshi_main');

define('IVR_COMPANY',    'Windshields To Go USA');
define('IVR_TRANSFER',   '+18005494470');
define('PROMPT_BASE',    'https://autoglasshosting.com/ivr/prompts/');
define('VIN_LINK_BASE',  'https://autoglasshosting.com/ivr/vin.php');
define('PAY_LINK_BASE',  'https://autoglasshosting.com/ivr/pay.php');
define('W2G_EMAIL',      'w2gusa@gmail.com');
define('SESSION_DIR',    '/tmp/ivr_sessions');
define('LOG_FILE',       '/tmp/ivr_twilio.log');

// Account profiles keyed by DNIS
$ACCOUNTS = array(
    '+18778072201' => array('code' => 'PTL',     'company' => 'Penske',  'ask_po' => true,  'po_prefill' => '',         'aff_id' => 0, 'requires_payment' => false),
    '+18778457544' => array('code' => 'LINDE',   'company' => 'Linde',   'ask_po' => true,  'po_prefill' => '',         'aff_id' => 0, 'requires_payment' => true),
    '+18772240909' => array('code' => 'LINCARE', 'company' => 'Lincare', 'ask_po' => true,  'po_prefill' => '16020568', 'aff_id' => 0, 'requires_payment' => false),
);
$DEFAULT_ACCOUNT = array('code' => 'W2G', 'company' => '', 'ask_po' => false, 'po_prefill' => '', 'ask_unit' => false, 'aff_id' => 0, 'requires_payment' => true);

// ─── BOOTSTRAP ────────────────────────────────────────────────────────────────
error_reporting(0);
if (!is_dir(SESSION_DIR)) mkdir(SESSION_DIR, 0700, true);

function ivr_log($msg) {
    file_put_contents(LOG_FILE, date('[Y-m-d H:i:s] ') . $msg . "\n", FILE_APPEND);
}

// ─── ERROR HANDLER ────────────────────────────────────────────────────────────
// Catches PHP errors and returns valid TwiML instead of crashing
register_shutdown_function(function() {
    $e = error_get_last();
    if ($e && in_array($e['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR))) {
        file_put_contents(LOG_FILE, date('[Y-m-d H:i:s] ') . 'PHP FATAL: ' . $e['message'] . ' in ' . $e['file'] . ':' . $e['line'] . "\n", FILE_APPEND);
        // Only output TwiML if headers not sent yet
        if (!headers_sent()) {
            header('Content-Type: text/xml');
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Say voice="alice">We apologize for the inconvenience. Please hold while we connect you to an agent.</Say>' . "\n";
            echo '  <Dial>+18005494470</Dial>' . "\n";
            echo '</Response>' . "\n";
        }
    }
});

// ─── VALIDATE TWILIO REQUEST ──────────────────────────────────────────────────
function validate_twilio() {
    // Basic validation — check expected Twilio params present
    if (empty($_POST['CallSid']) && empty($_GET['CallSid'])) {
        ivr_log('WARNING: Request missing CallSid — possible non-Twilio request');
    }
}

// ─── SESSION ──────────────────────────────────────────────────────────────────
function sess_path($id) {
    return SESSION_DIR . '/' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $id) . '.json';
}
function sess_load($id) {
    $f = sess_path($id);
    return file_exists($f) ? (json_decode(file_get_contents($f), true) ?: []) : [];
}
function sess_save($id, $data) {
    file_put_contents(sess_path($id), json_encode($data));
}
function sess_del($id) {
    $f = sess_path($id);
    if (file_exists($f)) unlink($f);
}

// ─── NAGS PART LOOKUP ────────────────────────────────────────────────────────
function lookup_nags($year, $make, $model, $style, $glass, $zip) {
    $opening_map = array(
        'windshield' => 'WS', 'driver' => 'DR', 'passenger' => 'DR',
        'rear' => 'BK', 'back' => 'BK', 'quarter' => 'QT',
    );
    $opening = 'WS';
    $glass_lower = strtolower($glass);
    foreach ($opening_map as $k => $v) {
        if (strpos($glass_lower, $k) !== false) { $opening = $v; break; }
    }

    // Try requested year first, then fall back up to 5 years earlier
    $years_to_try = array($year);
    $year_int = (int)$year;
    for ($i = 1; $i <= 5; $i++) {
        $years_to_try[] = (string)($year_int - $i);
    }

    foreach ($years_to_try as $try_year) {
        $url = 'https://autoglasshosting.com/ivr/proxy_nags_lookup.php'
            . '?year='    . urlencode($try_year)
            . '&make='    . urlencode($make)
            . '&model='   . urlencode($model)
            . '&style='   . urlencode($style)
            . '&opening=' . urlencode($opening)
            . '&zip='     . urlencode($zip);

        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_SSL_VERIFYPEER => false,
        ));
        $body = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($body, true);
        ivr_log("NAGS lookup $try_year $make $model '$style' opening=$opening: " . substr($body, 0, 200));

        // If we got a part return it
        if (!empty($data['part'])) {
            if ($try_year !== $year) {
                ivr_log("NAGS year fallback: $year → $try_year for $make $model");
            }
            return $data;
        }

        // If error is not "no styles found" stop trying
        if (empty($data['styles']) && isset($data['status']) && $data['status'] === 'error') {
            $err = isset($data['error']) ? $data['error'] : '';
            if (strpos(strtolower($err), 'style') === false && strpos(strtolower($err), 'body') === false) {
                break; // Not a year/style issue — stop trying
            }
        }
    }

    return $data ?: array();
}
// ─── YMM FUZZY MATCH (local NHTSA DB) ────────────────────────────────────────
function ymm_correct_make($spoken_make, $year) {
    $spoken = strtolower(trim($spoken_make));

    // ── Hardcoded STT corrections ─────────────────────────────────────────────
    $make_corrections = array(
        'chevy'        => 'Chevrolet',
        'chevi'        => 'Chevrolet',
        'vw'           => 'Volkswagen',
        'volkswagon'   => 'Volkswagen',
        'volkswagen'   => 'Volkswagen',
        'also again'   => 'Volkswagen',  // known Deepgram mishearing
        'also a game'  => 'Volkswagen',
        'benz'         => 'Mercedes-Benz',
        'mercedes'     => 'Mercedes-Benz',
        'hundai'       => 'Hyundai',
        'hyundia'      => 'Hyundai',
        'infinity'     => 'Infiniti',
        'dodge ram'    => 'Ram',
        'navistar'     => 'International',
        'freightliner' => 'Freightliner',
        'freight liner' => 'Freightliner',
        'kenworth'     => 'Kenworth',
        'ken worth'    => 'Kenworth',
        'peterbilt'    => 'Peterbilt',
        'peter built'  => 'Peterbilt',
        'international' => 'International',
        'sprinter'     => 'Mercedes-Benz',
    );

    if (isset($make_corrections[$spoken])) {
        ivr_log("Make hardcoded correction: '$spoken_make' → '{$make_corrections[$spoken]}'");
        return $make_corrections[$spoken];
    }

    // Also check partial matches
    foreach ($make_corrections as $wrong => $right) {
        if (strpos($spoken, $wrong) !== false) {
            ivr_log("Make partial correction: '$spoken_make' → '$right'");
            return $right;
        }
    }

    // ── NHTSA YMM DB fuzzy match ──────────────────────────────────────────────
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) return ucwords(strtolower($spoken_make));

    $year_int   = (int)$year;
    $normalized = $spoken;

    // Get all makes for this year
    $esc_year = (int)$year_int;
    $result = $db->query("SELECT DISTINCT make FROM ivr_ymm_cache WHERE year=$esc_year ORDER BY make");
    if (!$result || $result->num_rows === 0) {
        $db->close();
        return ucwords(strtolower($spoken_make));
    }

    $best_make  = '';
    $best_score = -1;

    while ($row = $result->fetch_assoc()) {
        $candidate = strtolower($row['make']);

        // Exact match
        if ($normalized === $candidate || $spoken === $candidate) { $best_make = $row['make']; break; }

        // Contains match
        $score = 0;
        if (strpos($candidate, $normalized) !== false) $score = 90;
        elseif (strpos($normalized, $candidate) !== false) $score = 85;
        else {
            // Word match + levenshtein
            $s_words = preg_split('/[\s\-]+/', $normalized);
            $c_words = preg_split('/[\s\-]+/', $candidate);
            $matches = 0;
            foreach ($s_words as $sw) {
                if (strlen($sw) < 2) continue;
                foreach ($c_words as $cw) {
                    if ($sw === $cw) { $matches += 3; break; }
                    if (strlen($sw) > 3 && levenshtein($sw, $cw) <= 2) { $matches += 2; break; }
                    if (strpos($cw, $sw) !== false) { $matches += 1; break; }
                }
            }
            $total = max(count($s_words), count($c_words));
            $score = $total > 0 ? min(80, intval(($matches / ($total * 3)) * 100)) : 0;
        }

        if ($score > $best_score) { $best_score = $score; $best_make = $row['make']; }
    }

    $db->close();

    if ($best_make && $best_score >= 50) {
        $best_make = ucwords(strtolower($best_make));
        ivr_log("YMM make: '$spoken_make' → '$best_make' (score=$best_score)");
        return $best_make;
    }
    return ucwords(strtolower($spoken_make));
}

function ymm_correct_model($spoken_model, $make, $year) {
    $spoken = strtolower(trim($spoken_model));

    // ── Hardcoded STT corrections ─────────────────────────────────────────────
    // Add known Deepgram mishearings here as you discover them
    $model_corrections = array(
        // Volkswagen
        'tigua'     => 'Tiguan',
        'tiqua'     => 'Tiguan',
        'take 1'    => 'Tiguan',
        'take one'  => 'Tiguan',
        'tegan'     => 'Tiguan',
        'teagan'    => 'Tiguan',
        't'         => 'Tiguan',  // single letter — likely cut off
        // Toyota
        'camery'    => 'Camry',
        'camrey'    => 'Camry',
        'corrolla'  => 'Corolla',
        'corrola'   => 'Corolla',
        'highlander' => 'Highlander',
        'sequioa'   => 'Sequoia',
        'tundra'    => 'Tundra',
        'tacoma'    => 'Tacoma',
        // Ford
        'f-150'     => 'F-150',
        'f150'      => 'F-150',
        'f 150'     => 'F-150',
        'f-250'     => 'F-250',
        'f250'      => 'F-250',
        'f 250'     => 'F-250',
        'exploror'  => 'Explorer',
        'exporer'   => 'Explorer',
        'esscape'   => 'Escape',
        // Chevrolet
        'silverado' => 'Silverado',
        'siverado'  => 'Silverado',
        'equinocks' => 'Equinox',
        'equanox'   => 'Equinox',
        'suburben'  => 'Suburban',
        // Honda
        'accordian' => 'Accord',
        'crv'       => 'CR-V',
        'cr-v'      => 'CR-V',
        'hrv'       => 'HR-V',
        'hr-v'      => 'HR-V',
        // Dodge/Ram
        'charger'   => 'Charger',
        'challanger' => 'Challenger',
        // Freightliner
        'cascadia'     => 'Cascadia',
        'm-2'          => 'M2',
        'm 2'          => 'M2',
        'm two'        => 'M2',
        'mtwo'         => 'M2',
        'm. two'       => 'M2',
        'em two'       => 'M2',
        'am two'       => 'M2',
        'em 2'         => 'M2',
        'am 2'         => 'M2',
        // Sprinter
        'sprinter'  => 'Sprinter',
    );

    if (isset($model_corrections[$spoken])) {
        ivr_log("Model hardcoded correction: '$spoken_model' → '{$model_corrections[$spoken]}'");
        return $model_corrections[$spoken];
    }

    // Partial match — only if spoken is a substring of the wrong key, not vice versa
    // This prevents 'mustang' matching 'tiguan' etc.
    foreach ($model_corrections as $wrong => $right) {
        if ($spoken === $wrong) {
            ivr_log("Model exact correction: '$spoken_model' → '$right'");
            return $right;
        }
    }

    // ── NHTSA YMM DB fuzzy match ──────────────────────────────────────────────
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) return ucwords(strtolower($spoken_model));
    $year_int = (int)$year;
    $esc_make = $db->real_escape_string($make);

    $result = $db->query("SELECT DISTINCT model FROM ivr_ymm_cache 
                          WHERE year=$year_int AND make='$esc_make' 
                          ORDER BY model");

    if (!$result || $result->num_rows === 0) {
        // Try partial make match
        $esc_make_like = $db->real_escape_string('%' . $make . '%');
        $result = $db->query("SELECT DISTINCT model FROM ivr_ymm_cache 
                              WHERE year=$year_int AND make LIKE '$esc_make_like' 
                              ORDER BY model");
    }

    if (!$result || $result->num_rows === 0) {
        $db->close();
        return ucwords(strtolower($spoken_model));
    }

    $best_model = '';
    $best_score = -1;

    while ($row = $result->fetch_assoc()) {
        $candidate = strtolower($row['model']);

        if ($spoken === $candidate) { $best_model = $row['model']; break; }

        $score = 0;
        if (strpos($candidate, $spoken) !== false) $score = 90;
        elseif (strpos($spoken, $candidate) !== false) $score = 85;
        else {
            $s_words = preg_split('/[\s\-\/]+/', $spoken);
            $c_words = preg_split('/[\s\-\/]+/', $candidate);
            $matches = 0;
            foreach ($s_words as $sw) {
                if (strlen($sw) < 2) continue;
                foreach ($c_words as $cw) {
                    if ($sw === $cw) { $matches += 3; break; }
                    if (strlen($sw) > 3 && levenshtein($sw, $cw) <= 2) { $matches += 2; break; }
                    if (strpos($cw, $sw) !== false || strpos($sw, $cw) !== false) { $matches += 1; break; }
                }
            }
            $total = max(count($s_words), count($c_words));
            $score = $total > 0 ? min(80, intval(($matches / ($total * 3)) * 100)) : 0;
        }

        if ($score > $best_score) { $best_score = $score; $best_model = $row['model']; }
    }

    $db->close();

    if ($best_model && $best_score >= 50) {
        ivr_log("YMM model: '$spoken_model' → '$best_model' (score=$best_score)");
        return $best_model;
    }
    return ucwords(strtolower($spoken_model));
}

// ─── DB PRICE LOOKUP (quote_inquiry) ─────────────────────────────────────────
function lookup_price_from_db($make, $model, $glass, $zip, $year = '') {
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) { ivr_log('Price DB error: ' . $db->connect_error); return null; }

    $glass_map = array(
        'windshield' => 'Windshield', 'driver' => 'Driver',
        'passenger'  => 'Passenger',  'rear'   => 'Back Glass',
        'back'       => 'Back Glass', 'quarter' => 'Quarter',
    );
    $glass_search = 'Windshield';
    foreach ($glass_map as $k => $v) {
        if (strpos(strtolower($glass), $k) !== false) { $glass_search = $v; break; }
    }

    $esc_make  = $db->real_escape_string($make);
    $esc_model = $db->real_escape_string($model);
    $esc_glass = $db->real_escape_string($glass_search);
    $esc_year  = $db->real_escape_string($year);

    $row = null;

    // Step 1: Exact year + make + model + glass, last 12 months
    if ($esc_year) {
        $sql = "SELECT MIN(amount) as min_price, parts, quoteData
                FROM quote_inquiry
                WHERE quoteData LIKE '$esc_year %$esc_make%$esc_model%$esc_glass%'
                AND amount > 0 AND Is_install='Y'
                AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                LIMIT 1";
        $r = $db->query($sql);
        if ($r && $r->num_rows > 0) $row = $r->fetch_assoc();
    }

    // Step 2: Make + model + glass, last 12 months (any year)
    if (!$row || !$row['min_price']) {
        $sql2 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_model%$esc_glass%'
                 AND amount > 0 AND Is_install='Y'
                 AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                 LIMIT 1";
        $r2 = $db->query($sql2);
        if ($r2 && $r2->num_rows > 0) $row = $r2->fetch_assoc();
    }

    // Step 3: No date restriction
    if (!$row || !$row['min_price']) {
        $sql3 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_model%$esc_glass%'
                 AND amount > 0 AND Is_install='Y' LIMIT 1";
        $r3 = $db->query($sql3);
        if ($r3 && $r3->num_rows > 0) $row = $r3->fetch_assoc();
    }

    // Step 4: Make + glass only (broader fallback)
    if (!$row || !$row['min_price']) {
        $sql4 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_glass%'
                 AND amount > 0 AND Is_install='Y'
                 AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                 LIMIT 1";
        $r4 = $db->query($sql4);
        if ($r4 && $r4->num_rows > 0) $row = $r4->fetch_assoc();
    }

    $db->close();

    if ($row && $row['min_price'] > 0) {
        ivr_log("Quote DB price: $year $make $model $glass = \${$row['min_price']} part={$row['parts']} from: {$row['quoteData']}");
        return array('price' => (float)$row['min_price'], 'nspart' => $row['parts'] ?? '', 'style' => '', 'source' => 'quote_inquiry');
    }
    ivr_log("No quote_inquiry price for $year $make $model $glass");
    return null;
}

// ─── NAGS + PROXY PRICE LOOKUP ───────────────────────────────────────────────
function lookup_price_from_nags($year, $make, $model, $style, $glass, $zip, $hint_part = '') {
    global $config;
    $proxy_auth = (isset($config['proxy_user']) ? $config['proxy_user'] : '') . ':' . (isset($config['proxy_pass']) ? $config['proxy_pass'] : '');

    // If we have a part number hint from DB, try pricing that directly first
    if ($hint_part) {
        $opening_map = array(
            'windshield' => 'WS', 'driver' => 'DR', 'passenger' => 'DR',
            'rear' => 'BK', 'back' => 'BK', 'quarter' => 'QT',
        );
        $opening = 'WS';
        foreach ($opening_map as $k => $v) {
            if (strpos(strtolower($glass), $k) !== false) { $opening = $v; break; }
        }
        $url = 'https://autoglasshosting.com/proxy/proxy_getprice.php'
            . '?part='    . urlencode($hint_part)
            . '&zip='     . urlencode($zip)
            . '&acct=w2gcsr'
            . '&opening=' . urlencode($opening);
        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_USERPWD        => $proxy_auth,
        ));
        $body = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($body, true);
        $installed = isset($data['price_installed']) ? (float)$data['price_installed'] : 0;
        if (!$installed && isset($data['price_glass']) && isset($data['price_install'])) {
            $installed = (float)$data['price_glass'] + (float)$data['price_install'];
        }
        if ($installed > 0) {
            ivr_log("DB part hint price: part=$hint_part price=$installed");
            return array('price' => $installed, 'nspart' => $hint_part, 'style' => '', 'source' => 'db_hint');
        }
    }

    // Full NAGS lookup
    $nags = lookup_nags($year, $make, $model, $style, $glass, $zip);
    if (empty($nags['part'])) return null;

    $part    = $nags['part'];
    $opening = $nags['opening'] ?? 'WS';

    $url = 'https://autoglasshosting.com/proxy/proxy_getprice.php'
        . '?part='    . urlencode($part)
        . '&zip='     . urlencode($zip)
        . '&acct=w2gcsr'
        . '&opening=' . urlencode($opening);

    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERPWD        => $proxy_auth,
    ));
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    ivr_log("NAGS price part=$part zip=$zip: " . substr($body, 0, 150));

    $installed = isset($data['price_installed']) ? (float)$data['price_installed'] : 0;
    if (!$installed && isset($data['price_glass']) && isset($data['price_install'])) {
        $installed = (float)$data['price_glass'] + (float)$data['price_install'];
    }

    if ($installed > 0) {
        return array('price' => $installed, 'nspart' => $part, 'style' => $nags['style'] ?? '', 'source' => 'nags');
    }
    return null;
}

// ─── VEHICLE EXTRACTION FROM SINGLE UTTERANCE ────────────────────────────────
function extract_vehicle($transcript) {
    $t = strtolower(trim($transcript));
    // Strip common spoken fillers before vehicle info
    $t = preg_replace('/^(it\'s|its|it is|i have|i\'ve got|i need|this is|we have|we\'ve got)\s+a?\s*/i', '', $t);
    $t = preg_replace('/\b(a|an)\s+(20\d\d|19\d\d)/i', '$2', $t);
    $result = array('year' => '', 'make' => '', 'model' => '', 'glass' => '');

    // Year — numeric first, then spoken
    preg_match('/\b(19[89]\d|20[012]\d|2030)\b/', $t, $ym);
    if ($ym) {
        $result['year'] = $ym[1];
    } else {
        // Spoken year patterns
        $spoken_years = array(
            'twenty twenty six' => '2026', 'twenty twenty five' => '2025',
            'twenty twenty four' => '2024', 'twenty twenty three' => '2023',
            'twenty twenty two' => '2022', 'twenty twenty one' => '2021',
            'twenty twenty'     => '2020', 'twenty nineteen'   => '2019',
            'twenty eighteen'   => '2018', 'twenty seventeen'  => '2017',
            'twenty sixteen'    => '2016', 'twenty fifteen'    => '2015',
            'twenty fourteen'   => '2014', 'twenty thirteen'   => '2013',
            'twenty twelve'     => '2012', 'twenty eleven'     => '2011',
            'twenty ten'        => '2010', 'twenty oh nine'    => '2009',
            'twenty oh eight'   => '2008', 'twenty oh seven'   => '2007',
            'twenty oh six'     => '2006', 'twenty oh five'    => '2005',
        );
        foreach ($spoken_years as $spoken => $year) {
            if (strpos($t, $spoken) !== false) {
                $result['year'] = $year;
                break;
            }
        }
    }

    // Makes — same list as voice-quote.html plus fleet vehicles
    $makes = array(
        'acura','alfa romeo','audi','bmw','bentley','buick','cadillac',
        'chevrolet','chevy','chrysler','dodge','ferrari','fiat','ford',
        'gmc','genesis','honda','hyundai','infiniti','jaguar','jeep',
        'kia','lamborghini','land rover','lexus','lincoln','maserati',
        'mazda','mercedes-benz','mercedes','mini','mitsubishi','nissan',
        'porsche','ram','rolls royce','subaru','tesla','toyota',
        'volkswagen','vw','volvo',
        // Fleet
        'freightliner','kenworth','peterbilt','international','isuzu',
        'hino','mack','sterling','sprinter','ford transit','transit',
    );
    // Common mishearings → correct make
    $make_fixes = array(
        'chevy' => 'Chevrolet', 'vw' => 'Volkswagen',
        'volkswagon' => 'Volkswagen', 'mercedes' => 'Mercedes-Benz',
        'merc' => 'Mercedes-Benz', 'benz' => 'Mercedes-Benz',
        'hundai' => 'Hyundai', 'hyundia' => 'Hyundai',
        'infinity' => 'Infiniti', 'transit' => 'Ford',
    );
    foreach ($makes as $make) {
        if (strpos($t, $make) !== false) {
            $result['make'] = isset($make_fixes[$make]) ? $make_fixes[$make] : ucwords($make);
            break;
        }
    }
    // Apply mishearing fixes
    foreach ($make_fixes as $wrong => $right) {
        if (strpos($t, $wrong) !== false && empty($result['make'])) {
            $result['make'] = $right;
            break;
        }
    }

    // Models — check fleet specific first, then passenger vehicles
    $models = array(
        // Fleet — most specific first
        'business class m2','business class','cascadia','prostar','lonestar',
        'w900','t680','t880','coronado','114sd','122sd','108sd',
        'm two','mtwo','m-2','m2',
        // Passenger
        'f-150','f150','f-250','f250','f-350','f350',
        'silverado','camry','accord','civic','altima','sentra',
        'corolla','rav4','cr-v','crv','pilot','odyssey',
        'highlander','4runner','tacoma','tundra','explorer',
        'escape','edge','fusion','mustang','malibu','equinox',
        'traverse','suburban','tahoe','yukon','sierra','canyon',
        'colorado','trailblazer','blazer','impala','camaro','corvette',
        'charger','challenger','durango','grand cherokee','wrangler',
        'cherokee','compass','renegade','pacifica','elantra','tucson',
        'santa fe','sonata','telluride','sportage','sorento','forte',
        'soul','carnival','pathfinder','rogue','murano','armada',
        'frontier','titan','versa','maxima','kicks',
        'model 3','model y','model s','model x','cybertruck',
        'outback','forester','impreza','legacy','crosstrek','ascent',
        'prius','sienna','venza','land cruiser','sequoia',
        'passat','jetta','golf','tiguan','atlas',
        'xc90','xc60','xc40','range rover','discovery','defender',
        'a4','a6','q5','q7','q3','x3','x5','x7',
        'gle','glc','escalade','xt5','xt6','enclave','encore',
        'navigator','aviator','tlx','mdx','rdx','ridgeline',
        '1500','2500','3500','transit','sprinter','express','savana',
        'wagoneer','gladiator','stelvio','giulia',
    );
    $model_normalize = array(
        'business class m2' => 'M2', 'business class' => 'M2', 'business m2' => 'M2',
        'm two' => 'M2', 'mtwo' => 'M2', 'm-2' => 'M2', 'm2' => 'M2',
        't680' => 'T680', 't880' => 'T880', 'w900' => 'W900',
        'cr-v' => 'CR-V', 'crv' => 'CR-V', 'hr-v' => 'HR-V',
        'rav4' => 'RAV4', '4runner' => '4Runner',
        'xc90' => 'XC90', 'xc60' => 'XC60', 'xc40' => 'XC40',
        'gle' => 'GLE', 'glc' => 'GLC',
        'mdx' => 'MDX', 'rdx' => 'RDX', 'tlx' => 'TLX',
        'xt5' => 'XT5', 'xt6' => 'XT6',
        '114sd' => '114SD', '122sd' => '122SD', '108sd' => '108SD',
        'a4' => 'A4', 'a6' => 'A6', 'q3' => 'Q3', 'q5' => 'Q5', 'q7' => 'Q7',
        'x3' => 'X3', 'x5' => 'X5', 'x7' => 'X7',
    );
    foreach ($models as $model) {
        if (strpos($t, $model) !== false) {
            $result['model'] = isset($model_normalize[$model]) ? $model_normalize[$model] : ucwords($model);
            break;
        }
    }

    // If model still empty, try NHTSA DB fuzzy match
    if (empty($result['model']) && !empty($result['make'])) {
        // Extract remaining words after make as potential model
        $make_lower = strtolower($result['make']);
        $remaining = trim(preg_replace('/' . preg_quote($make_lower, '/') . '/i', '', $t));
        $remaining = trim(preg_replace('/(20\d\d|19\d\d)/', '', $remaining));
        $remaining = trim(preg_replace('/(two|four|2|4)\s*(door|dr)/i', '', $remaining));
        $words = preg_split('/\s+/', trim($remaining));
        if (!empty($words[0])) {
            $year_for_lookup = !empty($result['year']) ? $result['year'] : date('Y');
            $corrected = ymm_correct_model($words[0], $result['make'], $year_for_lookup);
            if ($corrected && strtolower($corrected) !== strtolower($words[0])) {
                $result['model'] = $corrected;
            } elseif ($corrected) {
                $result['model'] = $corrected;
            }
        }
    }

    // Also fix make using NHTSA if it looks wrong
    if (!empty($result['make']) && !empty($result['year'])) {
        $corrected_make = ymm_correct_make($result['make'], $result['year']);
        if ($corrected_make) $result['make'] = $corrected_make;
    }

    // Glass type
    $glass_map = array(
        'windshield' => 'Windshield',
        'wind shield' => 'Windshield',
        'front glass' => 'Windshield',
        'rear' => 'Back Glass',
        'back glass' => 'Back Glass',
        'back window' => 'Back Glass',
        'driver' => 'Driver Side',
        'passenger' => 'Passenger Side',
        'quarter' => 'Quarter Glass',
    );
    foreach ($glass_map as $k => $v) {
        if (strpos($t, $k) !== false) { $result['glass'] = $v; break; }
    }
    // Default to windshield if glass type not specified
    if (empty($result['glass'])) $result['glass'] = 'Windshield';

    return $result;
}

// ─── WHISPER TRANSCRIPTION ────────────────────────────────────────────────────
function whisper_transcribe($recording_url) {
    global $config;
    if (empty($recording_url)) return '';
    $audio_url = rtrim($recording_url, '/') . '.mp3';
    $ch = curl_init($audio_url);
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERPWD        => ($config['twilio_account_sid'] ?? '') . ':' . ($config['twilio_auth_token'] ?? $config['twilio_api_secret'] ?? ''),
    ));
    $audio_data = curl_exec($ch);
    curl_close($ch);
    if (!$audio_data) { ivr_log('Whisper: failed to download audio from ' . $audio_url); return ''; }
    $tmp = tempnam('/tmp', 'ivr_audio_') . '.mp3';
    file_put_contents($tmp, $audio_data);
    $ch = curl_init('https://api.openai.com/v1/audio/transcriptions');
    $post = array(
        'file'     => new CURLFile($tmp, 'audio/mpeg', 'audio.mp3'),
        'model'    => 'whisper-1',
        'language' => 'en',
    );
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $post,
        CURLOPT_HTTPHEADER     => array('Authorization: Bearer ' . ($config['openai_api_key'] ?? '')),
        CURLOPT_TIMEOUT        => 60,
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    unlink($tmp);
    $data = json_decode($resp, true);
    $text = isset($data['text']) ? trim(rtrim(trim($data['text']), '. ')) : '';
    ivr_log('Whisper transcript: ' . substr($text, 0, 200));
    return $text;
}

// ─── CLAUDE ORDER EXTRACTION ──────────────────────────────────────────────────
function claude_extract_order($transcript, $acct) {
    global $config;
    if (empty($transcript)) return array();
    $account_code = isset($acct['code']) ? $acct['code'] : 'W2G';
    $is_fleet     = !empty($acct['ask_po']);
    $prompt = "Extract order information from this phone call transcript and return ONLY a JSON object with these exact fields:
year, make, model, style, glass, first, last, street, city, state, zip, phone, email, po, unit, notes, install_date

Rules:
- year: 4-digit vehicle year
- make: vehicle manufacturer (e.g. Volkswagen, Ford, Freightliner)
- model: vehicle model (e.g. Tiguan, F-150, Cascadia)
- style: body style (e.g. 4 Door Utility, 2 Door Coupe, Crew Cab)
- glass: type of glass needed (Windshield, Back Glass, Driver Side, Passenger Side, Quarter Glass)
- first/last: customer first and last name
- street: street address
- city, state, zip: location
- phone: 10 digit phone number digits only
- email: email address
- po: purchase order or RO number" . ($is_fleet ? " (this is a $account_code fleet account)" : '') . "
- unit: vehicle unit or fleet number
- notes: any special instructions
- install_date: requested service date YYYY-MM-DD or empty string

Leave missing fields as empty string. Return ONLY the JSON object.

Transcript: $transcript";

    $ch = curl_init('https://api.anthropic.com/v1/messages');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(array(
            'model'      => 'claude-sonnet-4-6',
            'max_tokens' => 1000,
            'messages'   => array(array('role' => 'user', 'content' => $prompt)),
        )),
        CURLOPT_HTTPHEADER => array(
            'Content-Type: application/json',
            'x-api-key: ' . ($config['anthropic_api_key'] ?? ''),
            'anthropic-version: 2023-06-01',
        ),
        CURLOPT_TIMEOUT => 30,
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($resp, true);
    $text = isset($data['content'][0]['text']) ? $data['content'][0]['text'] : '';
    $text = preg_replace('/^```json\s*/i', '', trim($text));
    $text = preg_replace('/```$/', '', trim($text));
    $fields = json_decode(trim($text), true);
    if (!is_array($fields)) {
        ivr_log('Claude extraction failed: ' . substr($resp, 0, 200));
        return array();
    }
    return $fields;
}

// ─── ZIP CODE LOOKUP ──────────────────────────────────────────────────────────
function zip_to_city_state($zip) {
    $ch = curl_init('https://api.zippopotam.us/us/' . urlencode($zip));
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_SSL_VERIFYPEER => false,
    ));
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    if (!empty($data['places'][0])) {
        return array(
            'city'  => $data['places'][0]['place name'] ?? '',
            'state' => $data['places'][0]['state abbreviation'] ?? '',
        );
    }
    return array('city' => '', 'state' => '');
}

// Record after playing prompt — mic opens after prompt finishes
// After recording, Twilio posts RecordingUrl back to $next_action
function twiml_record($prompt_mp3, $next_action, $call_sid, $max_length = 10) {
    $action_url = next_url($next_action, $call_sid);
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Pause length="1"/>' . "\n";
    echo '  <Record action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' maxLength="' . $max_length . '"'
        . ' timeout="3"'
        . ' transcribe="false"'
        . ' playBeep="false"'
        . ' finishOnKey="">' . "\n";
    echo '  </Record>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Process a recording with Whisper — plays wait message while processing
function whisper_process($recording_url, $wait_prompt = '37_onemomment.mp3') {
    // Start Whisper transcription (this takes 2-3 seconds)
    return whisper_transcribe($recording_url);
}

function base_url() {
    $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    return $proto . '://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
}

function next_url($action, $call_sid) {
    return base_url() . '?action=' . urlencode($action) . '&sid=' . urlencode($call_sid);
}

// Play an ElevenLabs MP3 prompt and gather speech response via Deepgram
function twiml_gather($prompt_mp3, $next_action, $call_sid, $timeout = 5, $hints = '') {
    $action_url = next_url($next_action, $call_sid);
    $hint_attr  = $hints ? ' hints="' . htmlspecialchars($hints) . '"' : '';
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Gather input="speech dtmf" action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' speechTimeout="3"'
        . ' actionOnEmptyResult="true"'
        . ' speechModel="deepgram_nova-2"'
        . ' profanityFilter="false"'
        . ' timeout="' . $timeout . '"'
        . $hint_attr . '>' . "\n";
    echo '    <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  </Gather>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars($action_url) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Play prompt THEN gather — no barge-in, prevents echo on critical fields
function twiml_play_then_gather($prompt_mp3, $next_action, $call_sid, $timeout = 8, $hints = '') {
    $action_url = next_url($next_action, $call_sid);
    $hint_attr  = $hints ? ' hints="' . htmlspecialchars($hints) . '"' : '';
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    // Play prompt first — mic closed during playback, no echo
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Gather input="speech dtmf" action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' speechTimeout="3"'
        . ' actionOnEmptyResult="true"'
        . ' speechModel="deepgram_nova-2"'
        . ' profanityFilter="false"'
        . ' timeout="' . $timeout . '"'
        . $hint_attr . '>' . "\n";
    echo '  </Gather>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars($action_url) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Play prompt with no gather (final message)
function twiml_play($prompt_mp3) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Hangup/>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_say($text) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Say voice="alice">' . htmlspecialchars($text) . '</Say>' . "\n";
    echo '  <Hangup/>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_transfer($number) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Dial>' . htmlspecialchars($number) . '</Dial>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_redirect($action, $call_sid) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars(next_url($action, $call_sid)) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_hangup() {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response><Hangup/></Response>' . "\n";
    exit;
}

// ─── RC SMS (via RingCentral) ─────────────────────────────────────────────────
function rc_token() {
    $cache = '/tmp/rc_tok_w2g.json';
    if (file_exists($cache)) {
        $t = json_decode(file_get_contents($cache), true);
        if ($t && isset($t['expires_at']) && $t['expires_at'] > time() + 60) return $t['access_token'];
    }
    global $config;
    $ch = curl_init(RC_SERVER . '/restapi/oauth/token');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query(array(
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion'  => isset($config['rc_jwt']) ? $config['rc_jwt'] : '',
        )),
        CURLOPT_HTTPHEADER     => array(
            'Content-Type: application/x-www-form-urlencoded',
            'Authorization: Basic ' . base64_encode($config['rc_sms_client_id'] . ':' . $config['rc_sms_client_secret']),
        ),
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    $d = json_decode($resp, true);
    if (empty($d['access_token'])) {
        ivr_log('RC token error: ' . $resp);
        return null;
    }
    $d['expires_at'] = time() + (int)(isset($d['expires_in']) ? $d['expires_in'] : 3600);
    file_put_contents($cache, json_encode($d));
    return $d['access_token'];
}
function rc_sms($to, $msg) {
    $tok = rc_token();
    if (!$tok) {
        ivr_log('RC SMS: no token');
        return false;
    }
    $to = preg_replace('/[^0-9+]/', '', $to);
    if (strlen($to) > 0 && $to[0] !== '+') $to = '+1' . ltrim($to, '1');
    $ch = curl_init(RC_SERVER . '/restapi/v1.0/account/~/extension/~/sms');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(array(
            'from' => array('phoneNumber' => RC_SMS_FROM),
            'to'   => array(array('phoneNumber' => $to)),
            'text' => $msg,
        )),
        CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Bearer ' . $tok),
    ));
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // If 401 — token expired, delete cache and retry once
    if ($code === 401) {
        ivr_log('RC SMS: 401 — refreshing token and retrying');
        @unlink('/tmp/rc_tok_w2g.json');
        $tok = rc_token();
        if (!$tok) return false;
        $ch = curl_init(RC_SERVER . '/restapi/v1.0/account/~/extension/~/sms');
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(array(
                'from' => array('phoneNumber' => RC_SMS_FROM),
                'to'   => array(array('phoneNumber' => $to)),
                'text' => $msg,
            )),
            CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Bearer ' . $tok),
        ));
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
    }

    if ($code >= 200 && $code < 300) return true;
    ivr_log('RC SMS failed: HTTP ' . $code . ' ' . substr($resp, 0, 100));
    return false;
}

// ─── EMAIL ────────────────────────────────────────────────────────────────────
function send_email($to, $subject, $html) {
    require_once '/home/auto11/public_html/vendor/autoload.php';
    try {
        $mail = new PHPMailer\PHPMailer\PHPMailer(true);
        $mail->isSMTP();
        $mail->Host       = 'smtp.gmail.com';
        $mail->SMTPAuth   = true;
        $mail->Username   = W2G_EMAIL;
        $mail->Password   = $GLOBALS['config']['gmail_app_password'] ?? '';
        $mail->SMTPSecure = 'ssl';
        $mail->Port       = 465;
        $mail->setFrom(W2G_EMAIL, IVR_COMPANY);
        $mail->addAddress($to);
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body    = $html;
        $mail->send();
        ivr_log("Email sent to $to");
        return true;
    } catch (Exception $e) {
        ivr_log('Email failed: ' . $e->getMessage());
        return false;
    }
}

// ─── WRITE ORDER TO DB ────────────────────────────────────────────────────────
function write_order(&$sess) {
    $db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) { ivr_log('DB error: ' . $db->connect_error); return false; }

    $acct         = $sess['account']  ?? [];
    $ocode        = $acct['code']     ?? 'W2G';
    $aff_id       = (int)($acct['aff_id'] ?? 0);
    $install_date = date('Y-m-d', strtotime('+1 day'));

    // Use quoted price for retail/Linde, $0 for AR fleet accounts
    $quoted_price = isset($sess['quoted_price']) ? (float)$sess['quoted_price'] : 0;
    $is_fleet     = (!empty($acct['ask_po']) || !empty($acct['ask_unit'])) && empty($acct['requires_payment']);
    $order_total  = (!$is_fleet && $quoted_price > 0) ? $quoted_price : 0;

    $f = [
        'zip'    => $db->real_escape_string($sess['zip']    ?? ''),
        'year'   => $db->real_escape_string($sess['year']   ?? ''),
        'make'   => $db->real_escape_string(ucwords(strtolower($sess['make']   ?? ''))),
        'model'  => $db->real_escape_string(ucwords(strtolower($sess['model']  ?? ''))),
        'style'  => $db->real_escape_string(ucwords(strtolower($sess['style']  ?? ''))),
        'glass'  => $db->real_escape_string(ucwords(strtolower($sess['glass']  ?? ''))),
        'first'  => $db->real_escape_string(ucwords(strtolower($sess['first']  ?? ''))),
        'last'   => $db->real_escape_string(ucwords(strtolower($sess['last']   ?? ''))),
        'street' => $db->real_escape_string(ucwords(strtolower($sess['street'] ?? ''))),
        'city'   => $db->real_escape_string(ucwords(strtolower($sess['city']   ?? ''))),
        'state'  => $db->real_escape_string(strtoupper($sess['state']  ?? '')),
        'phone'  => $db->real_escape_string($sess['phone']  ?? ''),
        'email'  => $db->real_escape_string(strtolower($sess['email']  ?? '')),
        'po'     => $db->real_escape_string(strtoupper($sess['po']     ?? ($acct['po_prefill'] ?? ''))),
        'unit'   => $db->real_escape_string(strtoupper($sess['unit']   ?? '')),
        'notes'  => $db->real_escape_string($sess['notes']  ?? ''),
        'company'=> $db->real_escape_string($acct['company'] ?? ''),
        'sid'    => $db->real_escape_string('ivr_' . ($sess['call_sid'] ?? '')),
    ];

    $nags_part  = $db->real_escape_string($sess['nags_part']  ?? '');
    $nags_style = $db->real_escape_string($sess['nags_style'] ?? ($sess['style'] ?? ''));

    // Look up productID from NAGS part number
    // nspart stores base part (e.g. FW5460) without suffix (GTY etc)
    $product_id = 0;
    if ($nags_part) {
        // Try exact match on nspart
        $row = $db->query("SELECT ID FROM product WHERE nspart='$nags_part' LIMIT 1");
        if ($row && $row->num_rows > 0) {
            $product_id = (int)$row->fetch_assoc()['ID'];
        }
        // Strip trailing letter suffix to get base part (FW5460GTY → FW5460)
        if (!$product_id) {
            $base_part = preg_replace('/^([A-Z]{1,3}\d+)[A-Z]+$/i', '$1', $nags_part);
            if ($base_part !== $nags_part) {
                $esc_base = $db->real_escape_string($base_part);
                $row2 = $db->query("SELECT ID FROM product WHERE nspart='$esc_base' LIMIT 1");
                if ($row2 && $row2->num_rows > 0) {
                    $product_id = (int)$row2->fetch_assoc()['ID'];
                    ivr_log("productID found via base part: $base_part → $product_id");
                }
            }
        }
        // Try part column as last resort
        if (!$product_id) {
            $row3 = $db->query("SELECT ID FROM product WHERE part='$nags_part' LIMIT 1");
            if ($row3 && $row3->num_rows > 0) {
                $product_id = (int)$row3->fetch_assoc()['ID'];
            }
        }
    }
    ivr_log("productID=$product_id for part=$nags_part");

    // cart
    $db->query("INSERT INTO cart (stamp, subtotal, zip, ip, session, randomKey)
                VALUES (Now(), 0, '{$f['zip']}', '127.0.0.1', '{$f['sid']}', '(ivr)')");
    $cart_id = $db->insert_id;
    if (!$cart_id) { ivr_log('cart insert failed: ' . $db->error); $db->close(); return false; }

    // cartItems — parameters='inst' flags as installation order, use real productID if found
    $db->query("INSERT INTO cartItems (cartID, productID, itemPrice, quantity, parameters, partDetails)
                VALUES ($cart_id, $product_id, 0, 1, 'inst', '{$f['zip']}')");

    // Build comments — vehicle summary + NAGS match + caller notes + CID
    $ani_comment = $db->real_escape_string($sess['ani'] ?? '');
    $vehicle_summary = "{$f['year']} {$f['make']} {$f['model']} — $nags_style — {$f['glass']}";
    if ($nags_part) $vehicle_summary .= " (NAGS: $nags_part)";
    $notes_clean = $f['notes'] ? "\nNotes: {$f['notes']}" : '';
    $vehicle_str = $db->real_escape_string("[IVR] $vehicle_summary$notes_clean\nCaller ID: $ani_comment");

    // checkout
    $db->query("INSERT INTO checkout SET
        cartID=$cart_id,
        s_FirstName='{$f['first']}', s_LastName='{$f['last']}',
        s_Email='{$f['email']}', s_Address='{$f['street']}',
        s_City='{$f['city']}', s_State='{$f['state']}',
        s_Zip='{$f['zip']}', s_Phone='{$f['phone']}',
        s_VIN='', s_Unit='{$f['unit']}', s_PO='{$f['po']}',
        s_Company='{$f['company']}', s_CrossStreet='',
        s_InstallYear='{$f['year']}',
        b_FirstName='{$f['first']}', b_LastName='{$f['last']}',
        b_Address='{$f['street']}', b_City='{$f['city']}',
        b_State='{$f['state']}', b_Zip='{$f['zip']}',
        b_Email='{$f['email']}', b_Phone='{$f['phone']}',
        comments='$vehicle_str', s_InstallDate='$install_date',
        b_SameAsShipping=0, b_Country='USA', s_Country='USA',
        c_num='', c_type='', c_expyear='', c_expmonth='', c_cvv='',
        subtotal=$order_total, shipping=0, total=$order_total");
    $checkout_id = $db->insert_id;

    // orderNums
    $esc_code = $db->real_escape_string($ocode);
    $db->query("INSERT INTO orderNums (stamp, refID, code, num)
                SELECT Now(), 0, '$esc_code',
                IF(MAX(num)+1 > 1, MAX(num)+1, 1)
                FROM orderNums WHERE code='$esc_code'");
    $onum_id = $db->insert_id;
    $orow    = $db->query("SELECT num FROM orderNums WHERE ID=$onum_id")->fetch_assoc();
    $onum    = (int)($orow['num'] ?? 1);
    $codenum = $ocode . '-' . sprintf('%04d', $onum);
    $ckey    = rand(1000, 9999);

    // invoice
    $db->query("INSERT INTO invoice SET
        checkoutID=$checkout_id, cartID=$cart_id,
        affiliateID=$aff_id, orderCode='$esc_code', orderNum=$onum,
        customerKey=$ckey, fulfilled='N',
        purchaseDate=Now(), installDate='$install_date'");
    $invoice_id = $db->insert_id;
    $db->close();

    if (!$invoice_id) { ivr_log('invoice insert failed'); return false; }

    // Write vehicle data to ivr_vehicles (before close)
    $v_year  = $db->real_escape_string($sess['year']    ?? '');
    $v_make  = $db->real_escape_string($sess['make']    ?? '');
    $v_model = $db->real_escape_string($sess['model']   ?? '');
    $v_style = $db->real_escape_string($sess['style']   ?? '');
    $v_glass = $db->real_escape_string($sess['glass']   ?? '');
    $v_acct  = $db->real_escape_string($acct['code']    ?? '');
    $db->query("INSERT INTO ivr_vehicles
        (invoice_id, cart_id, year, make, model, style, glass, zip, account, nags_part, nags_style)
        VALUES ($invoice_id, $cart_id, '$v_year', '$v_make', '$v_model',
                '$v_style', '$v_glass', '{$f['zip']}', '$v_acct',
                '$nags_part', '$nags_style')");

    $db->close();

    $sess['invoice_id']   = $invoice_id;
    $sess['order_num']    = $codenum;
    $sess['customer_key'] = $ckey;
    $sess['install_date'] = $install_date;

    ivr_log("Order created: $codenum invoiceID=$invoice_id");
    return true;
}

// ─── BUILD ORDER EMAIL ────────────────────────────────────────────────────────
function build_order_email(&$sess, $vin_url, $pay_url = '') {
    $acct    = $sess['account']      ?? array();
    $company = $acct['company']      ?? '';
    $onum    = $sess['order_num']    ?? '';
    $install = $sess['install_date'] ?? 'Tomorrow';
    $ani     = $sess['ani']          ?? '';
    $quoted  = isset($sess['quoted_price']) ? (float)$sess['quoted_price'] : 0;

    // Format ANI for display
    $ani_digits = preg_replace('/[^0-9]/', '', $ani);
    if (strlen($ani_digits) === 11 && $ani_digits[0] === '1') $ani_digits = substr($ani_digits, 1);
    $ani_display = strlen($ani_digits) === 10
        ? '(' . substr($ani_digits,0,3) . ') ' . substr($ani_digits,3,3) . '-' . substr($ani_digits,6)
        : $ani;

    return "
<html><body style='font-family:Arial,sans-serif;font-size:14px;'>
<h2 style='color:#003366;'>" . IVR_COMPANY . " - IVR Order</h2>
<table cellpadding='6' cellspacing='0' style='width:100%;max-width:620px;border-collapse:collapse;'>
  <tr><td colspan='2' style='background:#003366;color:#fff;padding:10px;font-weight:bold;font-size:16px;'>
    Order # $onum &nbsp;|&nbsp; $company
  </td></tr>
  <tr><td style='color:#666;width:35%;border-bottom:1px solid #eee;'>Install Date</td>
      <td style='border-bottom:1px solid #eee;'>$install <em style='color:#999;'>(default — subject to confirmation)</em></td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Vehicle</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['year'] ?? '') . ' ' . ($sess['make'] ?? '') . ' ' . ($sess['model'] ?? '') . ' ' . ($sess['style'] ?? '')) . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Glass</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['glass'] ?? '') . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Customer</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['first'] ?? '') . ' ' . ($sess['last'] ?? '')) . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Address</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['street'] ?? '') . ', ' . ($sess['city'] ?? '') . ', ' . ($sess['state'] ?? '') . ' ' . ($sess['zip'] ?? '')) . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Phone (spoken)</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['phone'] ?? '') . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'><strong>Caller ID (CID)</strong></td>
      <td style='border-bottom:1px solid #eee;'><strong style='color:#003366;'>$ani_display</strong></td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>PO / RO</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['po'] ?? '') . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Unit #</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['unit'] ?? '') . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Notes</td>
      <td style='border-bottom:1px solid #eee;'>" . nl2br(htmlspecialchars($sess['notes'] ?? '')) . "</td></tr>" .
  ($quoted > 0 ? "
  <tr><td style='color:#666;border-bottom:1px solid #eee;'><strong>Quoted Price</strong></td>
      <td style='border-bottom:1px solid #eee;'><strong style='color:#003366;'>\$$quoted installed</strong></td></tr>" : "") . "
  <tr><td colspan='2' style='padding:14px 6px;'>
    <strong>VIN Entry Link:</strong><br>
    <a href='$vin_url' style='color:#003366;'>$vin_url</a>
  </td></tr>" .
  ($pay_url ? "
  <tr><td colspan='2' style='padding:0 6px 14px;'>
    <strong>Payment Link:</strong><br>
    <a href='$pay_url' style='color:#cc0000;font-weight:bold;'>$pay_url</a>
  </td></tr>" : "") . "
  <tr><td colspan='2' style='padding:6px;background:#fff8e1;font-size:12px;color:#888;'>
    Order placed via IVR &nbsp;|&nbsp; Caller ID: $ani_display &nbsp;|&nbsp; Twilio SID: " . htmlspecialchars($sess['call_sid'] ?? '') . "
  </td></tr>
</table>
</body></html>";
}

// ─── MAIN ────────────────────────────────────────────────────────────────────
header('Content-Type: text/xml');
validate_twilio();

// Get Twilio params
$call_sid = $_POST['CallSid']      ?? $_GET['CallSid']      ?? '';
$ani      = $_POST['From']         ?? $_GET['From']         ?? '';
$dnis     = $_POST['To']           ?? $_GET['To']           ?? '';
$speech   = $_POST['SpeechResult'] ?? $_GET['SpeechResult'] ?? '';
$digit    = $_POST['Digits']       ?? $_GET['Digits']       ?? '';
// Deepgram often appends a trailing period — strip it globally
$said     = trim(rtrim(trim($speech ?: $digit), '. '));
$action   = $_GET['action']        ?? 'welcome';
$sid      = $_GET['sid']           ?? $call_sid;

// Normalize DNIS — Twilio passes RC forwarded number
$dnis_clean = '+1' . preg_replace('/[^0-9]/', '', $dnis);
if (strlen($dnis_clean) > 12) $dnis_clean = substr($dnis_clean, 0, 12);

ivr_log("[$sid] action=$action dnis_raw=$dnis dnis_clean=$dnis_clean ani=$ani ForwardedFrom=" . ($_POST["ForwardedFrom"] ?? "none") . " said=" . json_encode($said));

// Load/init session
global $ACCOUNTS, $DEFAULT_ACCOUNT;
$sess = sess_load($sid);
if (empty($sess)) {
    $acct = $ACCOUNTS[$dnis_clean] ?? $DEFAULT_ACCOUNT;
    $sess = [
        'call_sid' => $call_sid,
        'ani'      => $ani,
        'dnis'     => $dnis_clean,
        'account'  => $acct,
    ];
    sess_save($sid, $sess);
}
$acct = $sess['account'] ?? $DEFAULT_ACCOUNT;

// ── Universal key presses ─────────────────────────────────────────────────────
if ($digit === '3') {
    sess_del($sid);
    twiml_transfer(IVR_TRANSFER);
}
if ($digit === '2') {
    sess_del($sid);
    $acct_new = isset($ACCOUNTS[$dnis_clean]) ? $ACCOUNTS[$dnis_clean] : $DEFAULT_ACCOUNT;
    $sess = array('call_sid' => $call_sid, 'ani' => $ani, 'dnis' => $dnis_clean, 'account' => $acct_new);
    sess_save($sid, $sess);
    twiml_redirect('welcome', $sid);
}
if (($digit === '4' || stripos($said, 'order') !== false) && $action !== 'got_price_response') {
    twiml_play_then_gather('joined/07_name.mp3', 'got_name', $sid, 8);
}

// ─── STEP ROUTER ─────────────────────────────────────────────────────────────
switch ($action) {

    // ── WELCOME + ZIP ────────────────────────────────────────────────────────
    case 'welcome':
        twiml_record('joined/01_welcome.mp3', 'got_zip', $sid, 6);

    // ── GOT ZIP ──────────────────────────────────────────────────────────────
    case 'got_zip':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';

        // Processing pass - transcribe ZIP
        if (!empty($sess['zip_processing'])) {
            unset($sess['zip_processing']);
            $rec = isset($sess['rec_zip']) ? $sess['rec_zip'] : $recording_url;
            $zip_transcript = whisper_transcribe($rec);
            ivr_log("[$sid] ZIP transcript: $zip_transcript");
            $zip = preg_replace('/[^0-9]/', '', $zip_transcript);
            // Handle spoken zip like "nine three one oh three"
            if (strlen($zip) !== 5) {
                $zip_words = array('zero'=>'0','one'=>'1','two'=>'2','three'=>'3','four'=>'4','five'=>'5','six'=>'6','seven'=>'7','eight'=>'8','nine'=>'9','oh'=>'0','o'=>'0');
                $spoken = strtolower($zip_transcript);
                $zip_digits = '';
                foreach (preg_split('/[\s,]+/', $spoken) as $word) {
                    if (isset($zip_words[$word])) $zip_digits .= $zip_words[$word];
                    elseif (is_numeric($word)) $zip_digits .= $word;
                }
                if (strlen($zip_digits) === 5) $zip = $zip_digits;
            }
            if (strlen($zip) !== 5) {
                twiml_record('joined/02_zip_retry.mp3', 'got_zip', $sid, 6);
            }
            $location = zip_to_city_state($zip);
            $sess['zip']   = $zip;
            $sess['city']  = $location['city'];
            $sess['state'] = $location['state'];
            ivr_log("[$sid] ZIP $zip → {$location['city']}, {$location['state']}");
            sess_save($sid, $sess);
            twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);
        }

        if (empty($recording_url)) {
            twiml_record('joined/01_welcome.mp3', 'got_zip', $sid, 6);
        }
        // Store recording and play zipwait while we process
        $sess['rec_zip'] = $recording_url;
        $sess['zip_processing'] = true;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_zip', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    case 'process_zip':
        twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);

    // ── GOT GLASS ────────────────────────────────────────────────────────────
    case 'got_glass':
        // Processing pass — check FIRST before looking for RecordingUrl
        if (!empty($sess['glass_processing'])) {
            unset($sess['glass_processing']);
            $recording_url = isset($sess['rec_glass']) ? $sess['rec_glass'] : '';
        } else {
            $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
            if (empty($recording_url)) {
                twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);
            }
            // Store and play filler
            $sess['rec_glass'] = $recording_url;
            $sess['glass_processing'] = true;
            sess_save($sid, $sess);
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
            echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_glass', $sid)) . '</Redirect>' . "\n";
            echo '</Response>' . "\n";
            exit;
        }
        $glass_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Glass transcript: $glass_transcript");

        if (empty(trim($glass_transcript))) {
            twiml_record('joined/03_1_ask_glass_retry.mp3', 'got_glass', $sid, 3);
        }
        // Route based on windshield or other
        if (stripos($glass_transcript, 'other') !== false) {
            twiml_record('joined/04_otherglass.mp3', 'got_other_glass', $sid, 8);
        }
        // "windshield" or anything else = windshield
        $sess['glass'] = 'Windshield';
        sess_save($sid, $sess);
        twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);

    // ── GOT OTHER GLASS ───────────────────────────────────────────────────────
    case 'got_other_glass':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/04_otherglass.mp3', 'got_other_glass', $sid, 8);
        }
        $other_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Other glass transcript: $other_transcript");
        if (empty($other_transcript)) {
            twiml_record('joined/04_1_otherglass_retry.mp3', 'got_other_glass', $sid, 8);
        }
        $other = strtolower($other_transcript);
        $glass_map = array(
            'left front'    => 'Driver Side',
            'driver'        => 'Driver Side',
            'left rear'     => 'Driver Rear',
            'right front'   => 'Passenger Side',
            'passenger'     => 'Passenger Side',
            'right rear'    => 'Passenger Rear',
            'back'          => 'Back Glass',
            'rear'          => 'Back Glass',
            'quarter'       => 'Quarter Glass',
        );
        $glass_normalized = 'Back Glass'; // default if other
        foreach ($glass_map as $k => $v) {
            if (strpos($other, $k) !== false) { $glass_normalized = $v; break; }
        }
        $sess['glass'] = $glass_normalized;
        sess_save($sid, $sess);
        twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);

    // ── GOT YEAR/MAKE/MODEL/STYLE ─────────────────────────────────────────────
    case 'got_yearmake':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);
        }
        // Store recording URL in session and play wait prompt
        $sess['rec_yearmake'] = $recording_url;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('process_yearmake', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── PROCESS YEAR/MAKE/MODEL ───────────────────────────────────────────────
    case 'process_yearmake':
        $recording_url = isset($sess['rec_yearmake']) ? $sess['rec_yearmake'] : '';
        $transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] YMM transcript: $transcript");
        if (empty($transcript)) {
            twiml_record('joined/06_yearmake_retry.mp3', 'got_yearmake', $sid, 12);
        }
        $extracted = extract_vehicle($transcript);
        ivr_log("[$sid] YMM extract from '$transcript': " . json_encode($extracted));

        if (!empty($extracted['make']) && !empty($extracted['model'])) {
            $sess['year']  = !empty($extracted['year'])  ? $extracted['year']  : (isset($sess['year']) ? $sess['year'] : date('Y'));
            $sess['make']  = $extracted['make'];
            $sess['model'] = $extracted['model'];
            $style_raw = strtolower($transcript);
            if (strpos($style_raw, '2') !== false || strpos($style_raw, 'two') !== false) {
                $sess['style'] = '2 Door';
            } elseif (strpos($style_raw, '4') !== false || strpos($style_raw, 'four') !== false) {
                $sess['style'] = '4 Door';
            } else {
                $sess['style'] = '';
            }
            sess_save($sid, $sess);
            twiml_record('joined/07_name.mp3', 'got_name', $sid, 8);
        }
        twiml_record('joined/06_yearmake_retry.mp3', 'got_yearmake', $sid, 12);

    // ── GOT NAME ─────────────────────────────────────────────────────────────
    case 'got_name':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/07_name.mp3', 'got_name', $sid, 8);
        }
        if (empty($sess['name_processing'])) {
            $sess['rec_name'] = $recording_url;
            $sess['name_processing'] = true;
            sess_save($sid, $sess);
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
            echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_name', $sid)) . '</Redirect>' . "\n";
            echo '</Response>' . "\n";
            exit;
        }
        unset($sess['name_processing']);
        $recording_url = isset($sess['rec_name']) ? $sess['rec_name'] : $recording_url;
        $name_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Name transcript: $name_transcript");
        if (empty($name_transcript)) {
            twiml_record('joined/08_name_retry.mp3', 'got_name', $sid, 8);
        }
        // Strip common greeting phrases
        $name_lower = strtolower(trim($name_transcript));
        foreach (array('my name is', 'it\'s', 'this is', 'the name is') as $g) {
            $name_lower = trim(preg_replace('/^' . preg_quote($g, '/') . '\s*/i', '', $name_lower));
        }
        $parts = explode(' ', $name_lower, 2);
        $sess['first'] = ucfirst($parts[0] ?? '');
        $sess['last']  = ucwords($parts[1] ?? '');
        if (empty($sess['first'])) {
            twiml_record('joined/08_name_retry.mp3', 'got_name', $sid, 8);
        }
        sess_save($sid, $sess);
        twiml_redirect('got_street', $sid);

    // ── GOT STREET (recording → Whisper → address validation) ────────────────
    case 'got_street':
        // Processing pass — triggered after wait prompt
        if (!empty($sess['street_processing'])) {
            unset($sess['street_processing']);
            $rec = isset($sess['rec_street']) ? $sess['rec_street'] : '';
            if (empty($rec)) {
                twiml_record('joined/09_street.mp3', 'got_street', $sid, 15);
            }
            $street_transcript = whisper_transcribe($rec);
            ivr_log("[$sid] Street transcript: $street_transcript");
            if (empty($street_transcript)) {
                sess_save($sid, $sess);
                twiml_record('joined/10_street_retry.mp3', 'got_street', $sid, 15);
            }
            $street = ucwords(strtolower(trim($street_transcript)));
            // Address validation via Nominatim
            $zip     = isset($sess['zip']) ? $sess['zip'] : '';
            $query   = urlencode($street . ' ' . $zip . ' USA');
            $nom_url = 'https://nominatim.openstreetmap.org/search?q=' . $query . '&format=json&limit=1&addressdetails=1';
            $ch = curl_init($nom_url);
            curl_setopt_array($ch, array(
                CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_USERAGENT      => 'W2G-IVR/1.0 (w2gusa@gmail.com)',
            ));
            $nom_data = json_decode(curl_exec($ch), true);
            curl_close($ch);
            if (!empty($nom_data[0]['address'])) {
                $addr = $nom_data[0]['address'];
                $h = isset($addr['house_number']) ? $addr['house_number'] : '';
                $r = isset($addr['road'])         ? $addr['road']         : '';
                if ($h && $r) {
                    $validated = $h . ' ' . $r;
                    ivr_log("[$sid] Address validated: '$street' → '$validated'");
                    $street = $validated;
                }
            }
            $sess['street'] = $street;
            sess_save($sid, $sess);
            if (!empty($acct['ask_po'])) {
                twiml_gather('joined/11_PO.mp3', 'got_po', $sid, 6);
            } else {
                twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);
            }
        }

        // First pass — check for recording URL
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/09_street.mp3', 'got_street', $sid, 15);
        }

        // Recording received — store in session, set processing flag, play wait
        $sess['rec_street'] = $recording_url;
        $sess['street_processing'] = true;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_street', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── GOT PO ───────────────────────────────────────────────────────────────
    case 'got_po':
        $po = strtoupper(trim($said));
        $skip_po = (stripos($po, 'SKIP') !== false || stripos($po, 'PASS') !== false || empty($po));
        if ($skip_po) $po = '';
        if (!$skip_po && !empty($acct['po_prefill']) && empty($po)) $po = $acct['po_prefill'];
        if (empty($po) && !$skip_po) {
            twiml_gather('joined/12_PO_retry.mp3', 'got_po', $sid, 6);
        }
        $sess['po'] = $po;
        sess_save($sid, $sess);
        twiml_gather('joined/13_Unit.mp3', 'got_unit', $sid, 6);

    // ── GOT UNIT ─────────────────────────────────────────────────────────────
    case 'got_unit':
        $unit = strtoupper(trim($said));
        $skip_unit = (stripos($unit, 'SKIP') !== false || stripos($unit, 'PASS') !== false || empty($unit));
        if ($skip_unit) $unit = '';
        if (empty($unit) && !$skip_unit) {
            twiml_gather('joined/14_Unit_replay.mp3', 'got_unit', $sid, 6);
        }
        $sess['unit'] = $unit;
        sess_save($sid, $sess);
        twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);

    // ── GOT CONTACT (recording → Whisper → Claude extract) ───────────────────
    case 'got_contact':
        if (!empty($sess['contact_processing'])) {
            unset($sess['contact_processing']);
            $rec = $sess['rec_contact'] ?? '';
            if (!$rec) twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);
            $ct = whisper_transcribe($rec);
            ivr_log("[$sid] Contact transcript: $ct");
            if (!$ct) twiml_record('joined/16_contact_retry.mp3', 'got_contact', $sid, 30);
            $cf = claude_extract_order($ct, $acct);
            $email = $cf['email'] ?? '';
            $phone = $cf['phone'] ?? '';
            if (!$phone) { $d=preg_replace("/[^0-9]/","",$ct); if(strlen($d)>=10) $phone=substr($d,-10); }
            if (!$email) { $er=preg_replace("/\s+at\s+/i","@",strtolower($ct)); $er=preg_replace("/\s+dot\s+/i",".",$er); $er=preg_replace("/\s+/","",$er); if(strpos($er,"@")!==false) $email=$er; }
            $sess['email']=$email; $sess['phone']=$phone;
            sess_save($sid,$sess);
            twiml_gather('joined/17_cell.mp3','got_cell_confirm',$sid,5,'yes no');
        }
        $ru = $_POST['RecordingUrl'] ?? '';
        if (!$ru) twiml_record('joined/15_contact.mp3','got_contact',$sid,30);
        $sess['rec_contact']=$ru; $sess['contact_processing']=true;
        sess_save($sid,$sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>'."\\n";
        echo '<Response>'."\\n";
        echo '  <Play>'.PROMPT_BASE.'joined/silence_filler.mp3</Play>'."\\n";
        echo '  <Redirect method="POST">'.htmlspecialchars(next_url('got_contact',$sid)).'</Redirect>'."\\n";
        echo '</Response>'."\\n";
        exit;
<?php
/**
 * ivr.php — Windshields To Go USA · Twilio TwiML IVR Order System
 * Upload to: /home/auto11/public_html/ivr/ivr.php
 *
 * Twilio webhook URL: https://autoglasshosting.com/ivr/ivr.php
 *
 * Flow: Welcome+ZIP → Year → Make → Model → Style → Glass
 *       → First → Last → Street → City → State → Phone → Email
 *       → PO/RO → Unit → Notes → Write order → SMS + Email VIN link
 *
 * Account routing by Twilio "To" param (original RC DNIS passed through):
 *   +18778072201 → PTL   (Penske)
 *   +18778457544 → LINDE
 *   +18772240909 → LINCARE
 *
 * STT: Deepgram Nova-2 via Twilio <Gather>
 * Press 9 at any time to start over
 * Press 0 at any time to transfer to live agent
 */

// ─── CONFIG ───────────────────────────────────────────────────────────────────
$config = require '/home/auto11/var/config.php';

define('TWILIO_ACCOUNT_SID', $config['twilio_account_sid'] ?? '');
define('TWILIO_API_KEY_SID', $config['twilio_api_key_sid'] ?? '');
define('TWILIO_API_SECRET',  $config['twilio_api_secret']  ?? '');
define('TWILIO_NUMBER',      $config['twilio_number']      ?? '');
define('DEEPGRAM_API_KEY',   $config['deepgram_api_key']   ?? '');

define('RC_CLIENT_ID',       'XH2IPXECRWdczFx0giHgxh');
define('RC_CLIENT_SECRET',   'bOEeTScZ7pwepvzW5tA9gvXa1pQNT1zRZbgSyNBWJap8');
define('RC_SERVER',          'https://platform.ringcentral.com');
define('RC_SMS_FROM',        '+18059785161');

define('DB_HOST',  'localhost');
define('DB_USER',  'windshi_quote');
define('DB_PASS',  $config['db_password'] ?? '');
define('DB_NAME',  'windshi_main');

define('IVR_COMPANY',    'Windshields To Go USA');
define('IVR_TRANSFER',   '+18005494470');
define('PROMPT_BASE',    'https://autoglasshosting.com/ivr/prompts/');
define('VIN_LINK_BASE',  'https://autoglasshosting.com/ivr/vin.php');
define('PAY_LINK_BASE',  'https://autoglasshosting.com/ivr/pay.php');
define('W2G_EMAIL',      'w2gusa@gmail.com');
define('SESSION_DIR',    '/tmp/ivr_sessions');
define('LOG_FILE',       '/tmp/ivr_twilio.log');

// Account profiles keyed by DNIS
$ACCOUNTS = array(
    '+18778072201' => array('code' => 'PTL',     'company' => 'Penske',  'ask_po' => true,  'po_prefill' => '',         'aff_id' => 0, 'requires_payment' => false),
    '+18778457544' => array('code' => 'LINDE',   'company' => 'Linde',   'ask_po' => true,  'po_prefill' => '',         'aff_id' => 0, 'requires_payment' => true),
    '+18772240909' => array('code' => 'LINCARE', 'company' => 'Lincare', 'ask_po' => true,  'po_prefill' => '16020568', 'aff_id' => 0, 'requires_payment' => false),
);
$DEFAULT_ACCOUNT = array('code' => 'W2G', 'company' => '', 'ask_po' => false, 'po_prefill' => '', 'ask_unit' => false, 'aff_id' => 0, 'requires_payment' => true);

// ─── BOOTSTRAP ────────────────────────────────────────────────────────────────
error_reporting(0);
if (!is_dir(SESSION_DIR)) mkdir(SESSION_DIR, 0700, true);

function ivr_log($msg) {
    file_put_contents(LOG_FILE, date('[Y-m-d H:i:s] ') . $msg . "\n", FILE_APPEND);
}

// ─── ERROR HANDLER ────────────────────────────────────────────────────────────
// Catches PHP errors and returns valid TwiML instead of crashing
register_shutdown_function(function() {
    $e = error_get_last();
    if ($e && in_array($e['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR))) {
        file_put_contents(LOG_FILE, date('[Y-m-d H:i:s] ') . 'PHP FATAL: ' . $e['message'] . ' in ' . $e['file'] . ':' . $e['line'] . "\n", FILE_APPEND);
        // Only output TwiML if headers not sent yet
        if (!headers_sent()) {
            header('Content-Type: text/xml');
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Say voice="alice">We apologize for the inconvenience. Please hold while we connect you to an agent.</Say>' . "\n";
            echo '  <Dial>+18005494470</Dial>' . "\n";
            echo '</Response>' . "\n";
        }
    }
});

// ─── VALIDATE TWILIO REQUEST ──────────────────────────────────────────────────
function validate_twilio() {
    // Basic validation — check expected Twilio params present
    if (empty($_POST['CallSid']) && empty($_GET['CallSid'])) {
        ivr_log('WARNING: Request missing CallSid — possible non-Twilio request');
    }
}

// ─── SESSION ──────────────────────────────────────────────────────────────────
function sess_path($id) {
    return SESSION_DIR . '/' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $id) . '.json';
}
function sess_load($id) {
    $f = sess_path($id);
    return file_exists($f) ? (json_decode(file_get_contents($f), true) ?: []) : [];
}
function sess_save($id, $data) {
    file_put_contents(sess_path($id), json_encode($data));
}
function sess_del($id) {
    $f = sess_path($id);
    if (file_exists($f)) unlink($f);
}

// ─── NAGS PART LOOKUP ────────────────────────────────────────────────────────
function lookup_nags($year, $make, $model, $style, $glass, $zip) {
    $opening_map = array(
        'windshield' => 'WS', 'driver' => 'DR', 'passenger' => 'DR',
        'rear' => 'BK', 'back' => 'BK', 'quarter' => 'QT',
    );
    $opening = 'WS';
    $glass_lower = strtolower($glass);
    foreach ($opening_map as $k => $v) {
        if (strpos($glass_lower, $k) !== false) { $opening = $v; break; }
    }

    // Try requested year first, then fall back up to 5 years earlier
    $years_to_try = array($year);
    $year_int = (int)$year;
    for ($i = 1; $i <= 5; $i++) {
        $years_to_try[] = (string)($year_int - $i);
    }

    foreach ($years_to_try as $try_year) {
        $url = 'https://autoglasshosting.com/ivr/proxy_nags_lookup.php'
            . '?year='    . urlencode($try_year)
            . '&make='    . urlencode($make)
            . '&model='   . urlencode($model)
            . '&style='   . urlencode($style)
            . '&opening=' . urlencode($opening)
            . '&zip='     . urlencode($zip);

        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_SSL_VERIFYPEER => false,
        ));
        $body = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($body, true);
        ivr_log("NAGS lookup $try_year $make $model '$style' opening=$opening: " . substr($body, 0, 200));

        // If we got a part return it
        if (!empty($data['part'])) {
            if ($try_year !== $year) {
                ivr_log("NAGS year fallback: $year → $try_year for $make $model");
            }
            return $data;
        }

        // If error is not "no styles found" stop trying
        if (empty($data['styles']) && isset($data['status']) && $data['status'] === 'error') {
            $err = isset($data['error']) ? $data['error'] : '';
            if (strpos(strtolower($err), 'style') === false && strpos(strtolower($err), 'body') === false) {
                break; // Not a year/style issue — stop trying
            }
        }
    }

    return $data ?: array();
}
// ─── YMM FUZZY MATCH (local NHTSA DB) ────────────────────────────────────────
function ymm_correct_make($spoken_make, $year) {
    $spoken = strtolower(trim($spoken_make));

    // ── Hardcoded STT corrections ─────────────────────────────────────────────
    $make_corrections = array(
        'chevy'        => 'Chevrolet',
        'chevi'        => 'Chevrolet',
        'vw'           => 'Volkswagen',
        'volkswagon'   => 'Volkswagen',
        'volkswagen'   => 'Volkswagen',
        'also again'   => 'Volkswagen',  // known Deepgram mishearing
        'also a game'  => 'Volkswagen',
        'benz'         => 'Mercedes-Benz',
        'mercedes'     => 'Mercedes-Benz',
        'hundai'       => 'Hyundai',
        'hyundia'      => 'Hyundai',
        'infinity'     => 'Infiniti',
        'dodge ram'    => 'Ram',
        'navistar'     => 'International',
        'freightliner' => 'Freightliner',
        'freight liner' => 'Freightliner',
        'kenworth'     => 'Kenworth',
        'ken worth'    => 'Kenworth',
        'peterbilt'    => 'Peterbilt',
        'peter built'  => 'Peterbilt',
        'international' => 'International',
        'sprinter'     => 'Mercedes-Benz',
    );

    if (isset($make_corrections[$spoken])) {
        ivr_log("Make hardcoded correction: '$spoken_make' → '{$make_corrections[$spoken]}'");
        return $make_corrections[$spoken];
    }

    // Also check partial matches
    foreach ($make_corrections as $wrong => $right) {
        if (strpos($spoken, $wrong) !== false) {
            ivr_log("Make partial correction: '$spoken_make' → '$right'");
            return $right;
        }
    }

    // ── NHTSA YMM DB fuzzy match ──────────────────────────────────────────────
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) return ucwords(strtolower($spoken_make));

    $year_int   = (int)$year;
    $normalized = $spoken;

    // Get all makes for this year
    $esc_year = (int)$year_int;
    $result = $db->query("SELECT DISTINCT make FROM ivr_ymm_cache WHERE year=$esc_year ORDER BY make");
    if (!$result || $result->num_rows === 0) {
        $db->close();
        return ucwords(strtolower($spoken_make));
    }

    $best_make  = '';
    $best_score = -1;

    while ($row = $result->fetch_assoc()) {
        $candidate = strtolower($row['make']);

        // Exact match
        if ($normalized === $candidate || $spoken === $candidate) { $best_make = $row['make']; break; }

        // Contains match
        $score = 0;
        if (strpos($candidate, $normalized) !== false) $score = 90;
        elseif (strpos($normalized, $candidate) !== false) $score = 85;
        else {
            // Word match + levenshtein
            $s_words = preg_split('/[\s\-]+/', $normalized);
            $c_words = preg_split('/[\s\-]+/', $candidate);
            $matches = 0;
            foreach ($s_words as $sw) {
                if (strlen($sw) < 2) continue;
                foreach ($c_words as $cw) {
                    if ($sw === $cw) { $matches += 3; break; }
                    if (strlen($sw) > 3 && levenshtein($sw, $cw) <= 2) { $matches += 2; break; }
                    if (strpos($cw, $sw) !== false) { $matches += 1; break; }
                }
            }
            $total = max(count($s_words), count($c_words));
            $score = $total > 0 ? min(80, intval(($matches / ($total * 3)) * 100)) : 0;
        }

        if ($score > $best_score) { $best_score = $score; $best_make = $row['make']; }
    }

    $db->close();

    if ($best_make && $best_score >= 50) {
        $best_make = ucwords(strtolower($best_make));
        ivr_log("YMM make: '$spoken_make' → '$best_make' (score=$best_score)");
        return $best_make;
    }
    return ucwords(strtolower($spoken_make));
}

function ymm_correct_model($spoken_model, $make, $year) {
    $spoken = strtolower(trim($spoken_model));

    // ── Hardcoded STT corrections ─────────────────────────────────────────────
    // Add known Deepgram mishearings here as you discover them
    $model_corrections = array(
        // Volkswagen
        'tigua'     => 'Tiguan',
        'tiqua'     => 'Tiguan',
        'take 1'    => 'Tiguan',
        'take one'  => 'Tiguan',
        'tegan'     => 'Tiguan',
        'teagan'    => 'Tiguan',
        't'         => 'Tiguan',  // single letter — likely cut off
        // Toyota
        'camery'    => 'Camry',
        'camrey'    => 'Camry',
        'corrolla'  => 'Corolla',
        'corrola'   => 'Corolla',
        'highlander' => 'Highlander',
        'sequioa'   => 'Sequoia',
        'tundra'    => 'Tundra',
        'tacoma'    => 'Tacoma',
        // Ford
        'f-150'     => 'F-150',
        'f150'      => 'F-150',
        'f 150'     => 'F-150',
        'f-250'     => 'F-250',
        'f250'      => 'F-250',
        'f 250'     => 'F-250',
        'exploror'  => 'Explorer',
        'exporer'   => 'Explorer',
        'esscape'   => 'Escape',
        // Chevrolet
        'silverado' => 'Silverado',
        'siverado'  => 'Silverado',
        'equinocks' => 'Equinox',
        'equanox'   => 'Equinox',
        'suburben'  => 'Suburban',
        // Honda
        'accordian' => 'Accord',
        'crv'       => 'CR-V',
        'cr-v'      => 'CR-V',
        'hrv'       => 'HR-V',
        'hr-v'      => 'HR-V',
        // Dodge/Ram
        'charger'   => 'Charger',
        'challanger' => 'Challenger',
        // Freightliner
        'cascadia'     => 'Cascadia',
        'm-2'          => 'M2',
        'm 2'          => 'M2',
        'm two'        => 'M2',
        'mtwo'         => 'M2',
        'm. two'       => 'M2',
        'em two'       => 'M2',
        'am two'       => 'M2',
        'em 2'         => 'M2',
        'am 2'         => 'M2',
        // Sprinter
        'sprinter'  => 'Sprinter',
    );

    if (isset($model_corrections[$spoken])) {
        ivr_log("Model hardcoded correction: '$spoken_model' → '{$model_corrections[$spoken]}'");
        return $model_corrections[$spoken];
    }

    // Partial match — only if spoken is a substring of the wrong key, not vice versa
    // This prevents 'mustang' matching 'tiguan' etc.
    foreach ($model_corrections as $wrong => $right) {
        if ($spoken === $wrong) {
            ivr_log("Model exact correction: '$spoken_model' → '$right'");
            return $right;
        }
    }

    // ── NHTSA YMM DB fuzzy match ──────────────────────────────────────────────
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) return ucwords(strtolower($spoken_model));
    $year_int = (int)$year;
    $esc_make = $db->real_escape_string($make);

    $result = $db->query("SELECT DISTINCT model FROM ivr_ymm_cache 
                          WHERE year=$year_int AND make='$esc_make' 
                          ORDER BY model");

    if (!$result || $result->num_rows === 0) {
        // Try partial make match
        $esc_make_like = $db->real_escape_string('%' . $make . '%');
        $result = $db->query("SELECT DISTINCT model FROM ivr_ymm_cache 
                              WHERE year=$year_int AND make LIKE '$esc_make_like' 
                              ORDER BY model");
    }

    if (!$result || $result->num_rows === 0) {
        $db->close();
        return ucwords(strtolower($spoken_model));
    }

    $best_model = '';
    $best_score = -1;

    while ($row = $result->fetch_assoc()) {
        $candidate = strtolower($row['model']);

        if ($spoken === $candidate) { $best_model = $row['model']; break; }

        $score = 0;
        if (strpos($candidate, $spoken) !== false) $score = 90;
        elseif (strpos($spoken, $candidate) !== false) $score = 85;
        else {
            $s_words = preg_split('/[\s\-\/]+/', $spoken);
            $c_words = preg_split('/[\s\-\/]+/', $candidate);
            $matches = 0;
            foreach ($s_words as $sw) {
                if (strlen($sw) < 2) continue;
                foreach ($c_words as $cw) {
                    if ($sw === $cw) { $matches += 3; break; }
                    if (strlen($sw) > 3 && levenshtein($sw, $cw) <= 2) { $matches += 2; break; }
                    if (strpos($cw, $sw) !== false || strpos($sw, $cw) !== false) { $matches += 1; break; }
                }
            }
            $total = max(count($s_words), count($c_words));
            $score = $total > 0 ? min(80, intval(($matches / ($total * 3)) * 100)) : 0;
        }

        if ($score > $best_score) { $best_score = $score; $best_model = $row['model']; }
    }

    $db->close();

    if ($best_model && $best_score >= 50) {
        ivr_log("YMM model: '$spoken_model' → '$best_model' (score=$best_score)");
        return $best_model;
    }
    return ucwords(strtolower($spoken_model));
}

// ─── DB PRICE LOOKUP (quote_inquiry) ─────────────────────────────────────────
function lookup_price_from_db($make, $model, $glass, $zip, $year = '') {
    $db = new mysqli('localhost', DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) { ivr_log('Price DB error: ' . $db->connect_error); return null; }

    $glass_map = array(
        'windshield' => 'Windshield', 'driver' => 'Driver',
        'passenger'  => 'Passenger',  'rear'   => 'Back Glass',
        'back'       => 'Back Glass', 'quarter' => 'Quarter',
    );
    $glass_search = 'Windshield';
    foreach ($glass_map as $k => $v) {
        if (strpos(strtolower($glass), $k) !== false) { $glass_search = $v; break; }
    }

    $esc_make  = $db->real_escape_string($make);
    $esc_model = $db->real_escape_string($model);
    $esc_glass = $db->real_escape_string($glass_search);
    $esc_year  = $db->real_escape_string($year);

    $row = null;

    // Step 1: Exact year + make + model + glass, last 12 months
    if ($esc_year) {
        $sql = "SELECT MIN(amount) as min_price, parts, quoteData
                FROM quote_inquiry
                WHERE quoteData LIKE '$esc_year %$esc_make%$esc_model%$esc_glass%'
                AND amount > 0 AND Is_install='Y'
                AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                LIMIT 1";
        $r = $db->query($sql);
        if ($r && $r->num_rows > 0) $row = $r->fetch_assoc();
    }

    // Step 2: Make + model + glass, last 12 months (any year)
    if (!$row || !$row['min_price']) {
        $sql2 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_model%$esc_glass%'
                 AND amount > 0 AND Is_install='Y'
                 AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                 LIMIT 1";
        $r2 = $db->query($sql2);
        if ($r2 && $r2->num_rows > 0) $row = $r2->fetch_assoc();
    }

    // Step 3: No date restriction
    if (!$row || !$row['min_price']) {
        $sql3 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_model%$esc_glass%'
                 AND amount > 0 AND Is_install='Y' LIMIT 1";
        $r3 = $db->query($sql3);
        if ($r3 && $r3->num_rows > 0) $row = $r3->fetch_assoc();
    }

    // Step 4: Make + glass only (broader fallback)
    if (!$row || !$row['min_price']) {
        $sql4 = "SELECT MIN(amount) as min_price, parts, quoteData
                 FROM quote_inquiry
                 WHERE quoteData LIKE '%$esc_make%$esc_glass%'
                 AND amount > 0 AND Is_install='Y'
                 AND inquiry_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
                 LIMIT 1";
        $r4 = $db->query($sql4);
        if ($r4 && $r4->num_rows > 0) $row = $r4->fetch_assoc();
    }

    $db->close();

    if ($row && $row['min_price'] > 0) {
        ivr_log("Quote DB price: $year $make $model $glass = \${$row['min_price']} part={$row['parts']} from: {$row['quoteData']}");
        return array('price' => (float)$row['min_price'], 'nspart' => $row['parts'] ?? '', 'style' => '', 'source' => 'quote_inquiry');
    }
    ivr_log("No quote_inquiry price for $year $make $model $glass");
    return null;
}

// ─── NAGS + PROXY PRICE LOOKUP ───────────────────────────────────────────────
function lookup_price_from_nags($year, $make, $model, $style, $glass, $zip, $hint_part = '') {
    global $config;
    $proxy_auth = (isset($config['proxy_user']) ? $config['proxy_user'] : '') . ':' . (isset($config['proxy_pass']) ? $config['proxy_pass'] : '');

    // If we have a part number hint from DB, try pricing that directly first
    if ($hint_part) {
        $opening_map = array(
            'windshield' => 'WS', 'driver' => 'DR', 'passenger' => 'DR',
            'rear' => 'BK', 'back' => 'BK', 'quarter' => 'QT',
        );
        $opening = 'WS';
        foreach ($opening_map as $k => $v) {
            if (strpos(strtolower($glass), $k) !== false) { $opening = $v; break; }
        }
        $url = 'https://autoglasshosting.com/proxy/proxy_getprice.php'
            . '?part='    . urlencode($hint_part)
            . '&zip='     . urlencode($zip)
            . '&acct=w2gcsr'
            . '&opening=' . urlencode($opening);
        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_USERPWD        => $proxy_auth,
        ));
        $body = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($body, true);
        $installed = isset($data['price_installed']) ? (float)$data['price_installed'] : 0;
        if (!$installed && isset($data['price_glass']) && isset($data['price_install'])) {
            $installed = (float)$data['price_glass'] + (float)$data['price_install'];
        }
        if ($installed > 0) {
            ivr_log("DB part hint price: part=$hint_part price=$installed");
            return array('price' => $installed, 'nspart' => $hint_part, 'style' => '', 'source' => 'db_hint');
        }
    }

    // Full NAGS lookup
    $nags = lookup_nags($year, $make, $model, $style, $glass, $zip);
    if (empty($nags['part'])) return null;

    $part    = $nags['part'];
    $opening = $nags['opening'] ?? 'WS';

    $url = 'https://autoglasshosting.com/proxy/proxy_getprice.php'
        . '?part='    . urlencode($part)
        . '&zip='     . urlencode($zip)
        . '&acct=w2gcsr'
        . '&opening=' . urlencode($opening);

    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERPWD        => $proxy_auth,
    ));
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    ivr_log("NAGS price part=$part zip=$zip: " . substr($body, 0, 150));

    $installed = isset($data['price_installed']) ? (float)$data['price_installed'] : 0;
    if (!$installed && isset($data['price_glass']) && isset($data['price_install'])) {
        $installed = (float)$data['price_glass'] + (float)$data['price_install'];
    }

    if ($installed > 0) {
        return array('price' => $installed, 'nspart' => $part, 'style' => $nags['style'] ?? '', 'source' => 'nags');
    }
    return null;
}

// ─── VEHICLE EXTRACTION FROM SINGLE UTTERANCE ────────────────────────────────
function extract_vehicle($transcript) {
    $t = strtolower(trim($transcript));
    // Strip common spoken fillers before vehicle info
    $t = preg_replace('/^(it\'s|its|it is|i have|i\'ve got|i need|this is|we have|we\'ve got)\s+a?\s*/i', '', $t);
    $t = preg_replace('/\b(a|an)\s+(20\d\d|19\d\d)/i', '$2', $t);
    $result = array('year' => '', 'make' => '', 'model' => '', 'glass' => '');

    // Year — numeric first, then spoken
    preg_match('/\b(19[89]\d|20[012]\d|2030)\b/', $t, $ym);
    if ($ym) {
        $result['year'] = $ym[1];
    } else {
        // Spoken year patterns
        $spoken_years = array(
            'twenty twenty six' => '2026', 'twenty twenty five' => '2025',
            'twenty twenty four' => '2024', 'twenty twenty three' => '2023',
            'twenty twenty two' => '2022', 'twenty twenty one' => '2021',
            'twenty twenty'     => '2020', 'twenty nineteen'   => '2019',
            'twenty eighteen'   => '2018', 'twenty seventeen'  => '2017',
            'twenty sixteen'    => '2016', 'twenty fifteen'    => '2015',
            'twenty fourteen'   => '2014', 'twenty thirteen'   => '2013',
            'twenty twelve'     => '2012', 'twenty eleven'     => '2011',
            'twenty ten'        => '2010', 'twenty oh nine'    => '2009',
            'twenty oh eight'   => '2008', 'twenty oh seven'   => '2007',
            'twenty oh six'     => '2006', 'twenty oh five'    => '2005',
        );
        foreach ($spoken_years as $spoken => $year) {
            if (strpos($t, $spoken) !== false) {
                $result['year'] = $year;
                break;
            }
        }
    }

    // Makes — same list as voice-quote.html plus fleet vehicles
    $makes = array(
        'acura','alfa romeo','audi','bmw','bentley','buick','cadillac',
        'chevrolet','chevy','chrysler','dodge','ferrari','fiat','ford',
        'gmc','genesis','honda','hyundai','infiniti','jaguar','jeep',
        'kia','lamborghini','land rover','lexus','lincoln','maserati',
        'mazda','mercedes-benz','mercedes','mini','mitsubishi','nissan',
        'porsche','ram','rolls royce','subaru','tesla','toyota',
        'volkswagen','vw','volvo',
        // Fleet
        'freightliner','kenworth','peterbilt','international','isuzu',
        'hino','mack','sterling','sprinter','ford transit','transit',
    );
    // Common mishearings → correct make
    $make_fixes = array(
        'chevy' => 'Chevrolet', 'vw' => 'Volkswagen',
        'volkswagon' => 'Volkswagen', 'mercedes' => 'Mercedes-Benz',
        'merc' => 'Mercedes-Benz', 'benz' => 'Mercedes-Benz',
        'hundai' => 'Hyundai', 'hyundia' => 'Hyundai',
        'infinity' => 'Infiniti', 'transit' => 'Ford',
    );
    foreach ($makes as $make) {
        if (strpos($t, $make) !== false) {
            $result['make'] = isset($make_fixes[$make]) ? $make_fixes[$make] : ucwords($make);
            break;
        }
    }
    // Apply mishearing fixes
    foreach ($make_fixes as $wrong => $right) {
        if (strpos($t, $wrong) !== false && empty($result['make'])) {
            $result['make'] = $right;
            break;
        }
    }

    // Models — check fleet specific first, then passenger vehicles
    $models = array(
        // Fleet — most specific first
        'business class m2','business class','cascadia','prostar','lonestar',
        'w900','t680','t880','coronado','114sd','122sd','108sd',
        'm two','mtwo','m-2','m2',
        // Passenger
        'f-150','f150','f-250','f250','f-350','f350',
        'silverado','camry','accord','civic','altima','sentra',
        'corolla','rav4','cr-v','crv','pilot','odyssey',
        'highlander','4runner','tacoma','tundra','explorer',
        'escape','edge','fusion','mustang','malibu','equinox',
        'traverse','suburban','tahoe','yukon','sierra','canyon',
        'colorado','trailblazer','blazer','impala','camaro','corvette',
        'charger','challenger','durango','grand cherokee','wrangler',
        'cherokee','compass','renegade','pacifica','elantra','tucson',
        'santa fe','sonata','telluride','sportage','sorento','forte',
        'soul','carnival','pathfinder','rogue','murano','armada',
        'frontier','titan','versa','maxima','kicks',
        'model 3','model y','model s','model x','cybertruck',
        'outback','forester','impreza','legacy','crosstrek','ascent',
        'prius','sienna','venza','land cruiser','sequoia',
        'passat','jetta','golf','tiguan','atlas',
        'xc90','xc60','xc40','range rover','discovery','defender',
        'a4','a6','q5','q7','q3','x3','x5','x7',
        'gle','glc','escalade','xt5','xt6','enclave','encore',
        'navigator','aviator','tlx','mdx','rdx','ridgeline',
        '1500','2500','3500','transit','sprinter','express','savana',
        'wagoneer','gladiator','stelvio','giulia',
    );
    $model_normalize = array(
        'business class m2' => 'M2', 'business class' => 'M2', 'business m2' => 'M2',
        'm two' => 'M2', 'mtwo' => 'M2', 'm-2' => 'M2', 'm2' => 'M2',
        't680' => 'T680', 't880' => 'T880', 'w900' => 'W900',
        'cr-v' => 'CR-V', 'crv' => 'CR-V', 'hr-v' => 'HR-V',
        'rav4' => 'RAV4', '4runner' => '4Runner',
        'xc90' => 'XC90', 'xc60' => 'XC60', 'xc40' => 'XC40',
        'gle' => 'GLE', 'glc' => 'GLC',
        'mdx' => 'MDX', 'rdx' => 'RDX', 'tlx' => 'TLX',
        'xt5' => 'XT5', 'xt6' => 'XT6',
        '114sd' => '114SD', '122sd' => '122SD', '108sd' => '108SD',
        'a4' => 'A4', 'a6' => 'A6', 'q3' => 'Q3', 'q5' => 'Q5', 'q7' => 'Q7',
        'x3' => 'X3', 'x5' => 'X5', 'x7' => 'X7',
    );
    foreach ($models as $model) {
        if (strpos($t, $model) !== false) {
            $result['model'] = isset($model_normalize[$model]) ? $model_normalize[$model] : ucwords($model);
            break;
        }
    }

    // If model still empty, try NHTSA DB fuzzy match
    if (empty($result['model']) && !empty($result['make'])) {
        // Extract remaining words after make as potential model
        $make_lower = strtolower($result['make']);
        $remaining = trim(preg_replace('/' . preg_quote($make_lower, '/') . '/i', '', $t));
        $remaining = trim(preg_replace('/(20\d\d|19\d\d)/', '', $remaining));
        $remaining = trim(preg_replace('/(two|four|2|4)\s*(door|dr)/i', '', $remaining));
        $words = preg_split('/\s+/', trim($remaining));
        if (!empty($words[0])) {
            $year_for_lookup = !empty($result['year']) ? $result['year'] : date('Y');
            $corrected = ymm_correct_model($words[0], $result['make'], $year_for_lookup);
            if ($corrected && strtolower($corrected) !== strtolower($words[0])) {
                $result['model'] = $corrected;
            } elseif ($corrected) {
                $result['model'] = $corrected;
            }
        }
    }

    // Also fix make using NHTSA if it looks wrong
    if (!empty($result['make']) && !empty($result['year'])) {
        $corrected_make = ymm_correct_make($result['make'], $result['year']);
        if ($corrected_make) $result['make'] = $corrected_make;
    }

    // Glass type
    $glass_map = array(
        'windshield' => 'Windshield',
        'wind shield' => 'Windshield',
        'front glass' => 'Windshield',
        'rear' => 'Back Glass',
        'back glass' => 'Back Glass',
        'back window' => 'Back Glass',
        'driver' => 'Driver Side',
        'passenger' => 'Passenger Side',
        'quarter' => 'Quarter Glass',
    );
    foreach ($glass_map as $k => $v) {
        if (strpos($t, $k) !== false) { $result['glass'] = $v; break; }
    }
    // Default to windshield if glass type not specified
    if (empty($result['glass'])) $result['glass'] = 'Windshield';

    return $result;
}

// ─── WHISPER TRANSCRIPTION ────────────────────────────────────────────────────
function whisper_transcribe($recording_url) {
    global $config;
    if (empty($recording_url)) return '';
    $audio_url = rtrim($recording_url, '/') . '.mp3';
    $ch = curl_init($audio_url);
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERPWD        => ($config['twilio_account_sid'] ?? '') . ':' . ($config['twilio_auth_token'] ?? $config['twilio_api_secret'] ?? ''),
    ));
    $audio_data = curl_exec($ch);
    curl_close($ch);
    if (!$audio_data) { ivr_log('Whisper: failed to download audio from ' . $audio_url); return ''; }
    $tmp = tempnam('/tmp', 'ivr_audio_') . '.mp3';
    file_put_contents($tmp, $audio_data);
    $ch = curl_init('https://api.openai.com/v1/audio/transcriptions');
    $post = array(
        'file'     => new CURLFile($tmp, 'audio/mpeg', 'audio.mp3'),
        'model'    => 'whisper-1',
        'language' => 'en',
    );
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $post,
        CURLOPT_HTTPHEADER     => array('Authorization: Bearer ' . ($config['openai_api_key'] ?? '')),
        CURLOPT_TIMEOUT        => 60,
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    unlink($tmp);
    $data = json_decode($resp, true);
    $text = isset($data['text']) ? trim(rtrim(trim($data['text']), '. ')) : '';
    ivr_log('Whisper transcript: ' . substr($text, 0, 200));
    return $text;
}

// ─── CLAUDE ORDER EXTRACTION ──────────────────────────────────────────────────
function claude_extract_order($transcript, $acct) {
    global $config;
    if (empty($transcript)) return array();
    $account_code = isset($acct['code']) ? $acct['code'] : 'W2G';
    $is_fleet     = !empty($acct['ask_po']);
    $prompt = "Extract order information from this phone call transcript and return ONLY a JSON object with these exact fields:
year, make, model, style, glass, first, last, street, city, state, zip, phone, email, po, unit, notes, install_date

Rules:
- year: 4-digit vehicle year
- make: vehicle manufacturer (e.g. Volkswagen, Ford, Freightliner)
- model: vehicle model (e.g. Tiguan, F-150, Cascadia)
- style: body style (e.g. 4 Door Utility, 2 Door Coupe, Crew Cab)
- glass: type of glass needed (Windshield, Back Glass, Driver Side, Passenger Side, Quarter Glass)
- first/last: customer first and last name
- street: street address
- city, state, zip: location
- phone: 10 digit phone number digits only
- email: email address
- po: purchase order or RO number" . ($is_fleet ? " (this is a $account_code fleet account)" : '') . "
- unit: vehicle unit or fleet number
- notes: any special instructions
- install_date: requested service date YYYY-MM-DD or empty string

Leave missing fields as empty string. Return ONLY the JSON object.

Transcript: $transcript";

    $ch = curl_init('https://api.anthropic.com/v1/messages');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(array(
            'model'      => 'claude-sonnet-4-6',
            'max_tokens' => 1000,
            'messages'   => array(array('role' => 'user', 'content' => $prompt)),
        )),
        CURLOPT_HTTPHEADER => array(
            'Content-Type: application/json',
            'x-api-key: ' . ($config['anthropic_api_key'] ?? ''),
            'anthropic-version: 2023-06-01',
        ),
        CURLOPT_TIMEOUT => 30,
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($resp, true);
    $text = isset($data['content'][0]['text']) ? $data['content'][0]['text'] : '';
    $text = preg_replace('/^```json\s*/i', '', trim($text));
    $text = preg_replace('/```$/', '', trim($text));
    $fields = json_decode(trim($text), true);
    if (!is_array($fields)) {
        ivr_log('Claude extraction failed: ' . substr($resp, 0, 200));
        return array();
    }
    return $fields;
}

// ─── ZIP CODE LOOKUP ──────────────────────────────────────────────────────────
function zip_to_city_state($zip) {
    $ch = curl_init('https://api.zippopotam.us/us/' . urlencode($zip));
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_SSL_VERIFYPEER => false,
    ));
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    if (!empty($data['places'][0])) {
        return array(
            'city'  => $data['places'][0]['place name'] ?? '',
            'state' => $data['places'][0]['state abbreviation'] ?? '',
        );
    }
    return array('city' => '', 'state' => '');
}

// Record after playing prompt — mic opens after prompt finishes
// After recording, Twilio posts RecordingUrl back to $next_action
function twiml_record($prompt_mp3, $next_action, $call_sid, $max_length = 10) {
    $action_url = next_url($next_action, $call_sid);
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Pause length="1"/>' . "\n";
    echo '  <Record action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' maxLength="' . $max_length . '"'
        . ' timeout="3"'
        . ' transcribe="false"'
        . ' playBeep="false"'
        . ' finishOnKey="">' . "\n";
    echo '  </Record>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Process a recording with Whisper — plays wait message while processing
function whisper_process($recording_url, $wait_prompt = '37_onemomment.mp3') {
    // Start Whisper transcription (this takes 2-3 seconds)
    return whisper_transcribe($recording_url);
}

function base_url() {
    $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    return $proto . '://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
}

function next_url($action, $call_sid) {
    return base_url() . '?action=' . urlencode($action) . '&sid=' . urlencode($call_sid);
}

// Play an ElevenLabs MP3 prompt and gather speech response via Deepgram
function twiml_gather($prompt_mp3, $next_action, $call_sid, $timeout = 5, $hints = '') {
    $action_url = next_url($next_action, $call_sid);
    $hint_attr  = $hints ? ' hints="' . htmlspecialchars($hints) . '"' : '';
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Gather input="speech dtmf" action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' speechTimeout="3"'
        . ' actionOnEmptyResult="true"'
        . ' speechModel="deepgram_nova-2"'
        . ' profanityFilter="false"'
        . ' timeout="' . $timeout . '"'
        . $hint_attr . '>' . "\n";
    echo '    <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  </Gather>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars($action_url) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Play prompt THEN gather — no barge-in, prevents echo on critical fields
function twiml_play_then_gather($prompt_mp3, $next_action, $call_sid, $timeout = 8, $hints = '') {
    $action_url = next_url($next_action, $call_sid);
    $hint_attr  = $hints ? ' hints="' . htmlspecialchars($hints) . '"' : '';
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    // Play prompt first — mic closed during playback, no echo
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Gather input="speech dtmf" action="' . htmlspecialchars($action_url) . '" method="POST"'
        . ' speechTimeout="3"'
        . ' actionOnEmptyResult="true"'
        . ' speechModel="deepgram_nova-2"'
        . ' profanityFilter="false"'
        . ' timeout="' . $timeout . '"'
        . $hint_attr . '>' . "\n";
    echo '  </Gather>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars($action_url) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

// Play prompt with no gather (final message)
function twiml_play($prompt_mp3) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Play>' . PROMPT_BASE . htmlspecialchars($prompt_mp3) . '</Play>' . "\n";
    echo '  <Hangup/>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_say($text) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Say voice="alice">' . htmlspecialchars($text) . '</Say>' . "\n";
    echo '  <Hangup/>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_transfer($number) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Dial>' . htmlspecialchars($number) . '</Dial>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_redirect($action, $call_sid) {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response>' . "\n";
    echo '  <Redirect method="POST">' . htmlspecialchars(next_url($action, $call_sid)) . '</Redirect>' . "\n";
    echo '</Response>' . "\n";
    exit;
}

function twiml_hangup() {
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<Response><Hangup/></Response>' . "\n";
    exit;
}

// ─── RC SMS (via RingCentral) ─────────────────────────────────────────────────
function rc_token() {
    $cache = '/tmp/rc_tok_w2g.json';
    if (file_exists($cache)) {
        $t = json_decode(file_get_contents($cache), true);
        if ($t && isset($t['expires_at']) && $t['expires_at'] > time() + 60) return $t['access_token'];
    }
    global $config;
    $ch = curl_init(RC_SERVER . '/restapi/oauth/token');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query(array(
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion'  => isset($config['rc_jwt']) ? $config['rc_jwt'] : '',
        )),
        CURLOPT_HTTPHEADER     => array(
            'Content-Type: application/x-www-form-urlencoded',
            'Authorization: Basic ' . base64_encode($config['rc_sms_client_id'] . ':' . $config['rc_sms_client_secret']),
        ),
    ));
    $resp = curl_exec($ch);
    curl_close($ch);
    $d = json_decode($resp, true);
    if (empty($d['access_token'])) {
        ivr_log('RC token error: ' . $resp);
        return null;
    }
    $d['expires_at'] = time() + (int)(isset($d['expires_in']) ? $d['expires_in'] : 3600);
    file_put_contents($cache, json_encode($d));
    return $d['access_token'];
}
function rc_sms($to, $msg) {
    $tok = rc_token();
    if (!$tok) {
        ivr_log('RC SMS: no token');
        return false;
    }
    $to = preg_replace('/[^0-9+]/', '', $to);
    if (strlen($to) > 0 && $to[0] !== '+') $to = '+1' . ltrim($to, '1');
    $ch = curl_init(RC_SERVER . '/restapi/v1.0/account/~/extension/~/sms');
    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(array(
            'from' => array('phoneNumber' => RC_SMS_FROM),
            'to'   => array(array('phoneNumber' => $to)),
            'text' => $msg,
        )),
        CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Bearer ' . $tok),
    ));
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // If 401 — token expired, delete cache and retry once
    if ($code === 401) {
        ivr_log('RC SMS: 401 — refreshing token and retrying');
        @unlink('/tmp/rc_tok_w2g.json');
        $tok = rc_token();
        if (!$tok) return false;
        $ch = curl_init(RC_SERVER . '/restapi/v1.0/account/~/extension/~/sms');
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(array(
                'from' => array('phoneNumber' => RC_SMS_FROM),
                'to'   => array(array('phoneNumber' => $to)),
                'text' => $msg,
            )),
            CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Bearer ' . $tok),
        ));
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
    }

    if ($code >= 200 && $code < 300) return true;
    ivr_log('RC SMS failed: HTTP ' . $code . ' ' . substr($resp, 0, 100));
    return false;
}

// ─── EMAIL ────────────────────────────────────────────────────────────────────
function send_email($to, $subject, $html) {
    require_once '/home/auto11/public_html/vendor/autoload.php';
    try {
        $mail = new PHPMailer\PHPMailer\PHPMailer(true);
        $mail->isSMTP();
        $mail->Host       = 'smtp.gmail.com';
        $mail->SMTPAuth   = true;
        $mail->Username   = W2G_EMAIL;
        $mail->Password   = $GLOBALS['config']['gmail_app_password'] ?? '';
        $mail->SMTPSecure = 'ssl';
        $mail->Port       = 465;
        $mail->setFrom(W2G_EMAIL, IVR_COMPANY);
        $mail->addAddress($to);
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body    = $html;
        $mail->send();
        ivr_log("Email sent to $to");
        return true;
    } catch (Exception $e) {
        ivr_log('Email failed: ' . $e->getMessage());
        return false;
    }
}

// ─── WRITE ORDER TO DB ────────────────────────────────────────────────────────
function write_order(&$sess) {
    $db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
    if ($db->connect_error) { ivr_log('DB error: ' . $db->connect_error); return false; }

    $acct         = $sess['account']  ?? [];
    $ocode        = $acct['code']     ?? 'W2G';
    $aff_id       = (int)($acct['aff_id'] ?? 0);
    $install_date = date('Y-m-d', strtotime('+1 day'));

    // Use quoted price for retail/Linde, $0 for AR fleet accounts
    $quoted_price = isset($sess['quoted_price']) ? (float)$sess['quoted_price'] : 0;
    $is_fleet     = (!empty($acct['ask_po']) || !empty($acct['ask_unit'])) && empty($acct['requires_payment']);
    $order_total  = (!$is_fleet && $quoted_price > 0) ? $quoted_price : 0;

    $f = [
        'zip'    => $db->real_escape_string($sess['zip']    ?? ''),
        'year'   => $db->real_escape_string($sess['year']   ?? ''),
        'make'   => $db->real_escape_string(ucwords(strtolower($sess['make']   ?? ''))),
        'model'  => $db->real_escape_string(ucwords(strtolower($sess['model']  ?? ''))),
        'style'  => $db->real_escape_string(ucwords(strtolower($sess['style']  ?? ''))),
        'glass'  => $db->real_escape_string(ucwords(strtolower($sess['glass']  ?? ''))),
        'first'  => $db->real_escape_string(ucwords(strtolower($sess['first']  ?? ''))),
        'last'   => $db->real_escape_string(ucwords(strtolower($sess['last']   ?? ''))),
        'street' => $db->real_escape_string(ucwords(strtolower($sess['street'] ?? ''))),
        'city'   => $db->real_escape_string(ucwords(strtolower($sess['city']   ?? ''))),
        'state'  => $db->real_escape_string(strtoupper($sess['state']  ?? '')),
        'phone'  => $db->real_escape_string($sess['phone']  ?? ''),
        'email'  => $db->real_escape_string(strtolower($sess['email']  ?? '')),
        'po'     => $db->real_escape_string(strtoupper($sess['po']     ?? ($acct['po_prefill'] ?? ''))),
        'unit'   => $db->real_escape_string(strtoupper($sess['unit']   ?? '')),
        'notes'  => $db->real_escape_string($sess['notes']  ?? ''),
        'company'=> $db->real_escape_string($acct['company'] ?? ''),
        'sid'    => $db->real_escape_string('ivr_' . ($sess['call_sid'] ?? '')),
    ];

    $nags_part  = $db->real_escape_string($sess['nags_part']  ?? '');
    $nags_style = $db->real_escape_string($sess['nags_style'] ?? ($sess['style'] ?? ''));

    // Look up productID from NAGS part number
    // nspart stores base part (e.g. FW5460) without suffix (GTY etc)
    $product_id = 0;
    if ($nags_part) {
        // Try exact match on nspart
        $row = $db->query("SELECT ID FROM product WHERE nspart='$nags_part' LIMIT 1");
        if ($row && $row->num_rows > 0) {
            $product_id = (int)$row->fetch_assoc()['ID'];
        }
        // Strip trailing letter suffix to get base part (FW5460GTY → FW5460)
        if (!$product_id) {
            $base_part = preg_replace('/^([A-Z]{1,3}\d+)[A-Z]+$/i', '$1', $nags_part);
            if ($base_part !== $nags_part) {
                $esc_base = $db->real_escape_string($base_part);
                $row2 = $db->query("SELECT ID FROM product WHERE nspart='$esc_base' LIMIT 1");
                if ($row2 && $row2->num_rows > 0) {
                    $product_id = (int)$row2->fetch_assoc()['ID'];
                    ivr_log("productID found via base part: $base_part → $product_id");
                }
            }
        }
        // Try part column as last resort
        if (!$product_id) {
            $row3 = $db->query("SELECT ID FROM product WHERE part='$nags_part' LIMIT 1");
            if ($row3 && $row3->num_rows > 0) {
                $product_id = (int)$row3->fetch_assoc()['ID'];
            }
        }
    }
    ivr_log("productID=$product_id for part=$nags_part");

    // cart
    $db->query("INSERT INTO cart (stamp, subtotal, zip, ip, session, randomKey)
                VALUES (Now(), 0, '{$f['zip']}', '127.0.0.1', '{$f['sid']}', '(ivr)')");
    $cart_id = $db->insert_id;
    if (!$cart_id) { ivr_log('cart insert failed: ' . $db->error); $db->close(); return false; }

    // cartItems — parameters='inst' flags as installation order, use real productID if found
    $db->query("INSERT INTO cartItems (cartID, productID, itemPrice, quantity, parameters, partDetails)
                VALUES ($cart_id, $product_id, 0, 1, 'inst', '{$f['zip']}')");

    // Build comments — vehicle summary + NAGS match + caller notes + CID
    $ani_comment = $db->real_escape_string($sess['ani'] ?? '');
    $vehicle_summary = "{$f['year']} {$f['make']} {$f['model']} — $nags_style — {$f['glass']}";
    if ($nags_part) $vehicle_summary .= " (NAGS: $nags_part)";
    $notes_clean = $f['notes'] ? "\nNotes: {$f['notes']}" : '';
    $vehicle_str = $db->real_escape_string("[IVR] $vehicle_summary$notes_clean\nCaller ID: $ani_comment");

    // checkout
    $db->query("INSERT INTO checkout SET
        cartID=$cart_id,
        s_FirstName='{$f['first']}', s_LastName='{$f['last']}',
        s_Email='{$f['email']}', s_Address='{$f['street']}',
        s_City='{$f['city']}', s_State='{$f['state']}',
        s_Zip='{$f['zip']}', s_Phone='{$f['phone']}',
        s_VIN='', s_Unit='{$f['unit']}', s_PO='{$f['po']}',
        s_Company='{$f['company']}', s_CrossStreet='',
        s_InstallYear='{$f['year']}',
        b_FirstName='{$f['first']}', b_LastName='{$f['last']}',
        b_Address='{$f['street']}', b_City='{$f['city']}',
        b_State='{$f['state']}', b_Zip='{$f['zip']}',
        b_Email='{$f['email']}', b_Phone='{$f['phone']}',
        comments='$vehicle_str', s_InstallDate='$install_date',
        b_SameAsShipping=0, b_Country='USA', s_Country='USA',
        c_num='', c_type='', c_expyear='', c_expmonth='', c_cvv='',
        subtotal=$order_total, shipping=0, total=$order_total");
    $checkout_id = $db->insert_id;

    // orderNums
    $esc_code = $db->real_escape_string($ocode);
    $db->query("INSERT INTO orderNums (stamp, refID, code, num)
                SELECT Now(), 0, '$esc_code',
                IF(MAX(num)+1 > 1, MAX(num)+1, 1)
                FROM orderNums WHERE code='$esc_code'");
    $onum_id = $db->insert_id;
    $orow    = $db->query("SELECT num FROM orderNums WHERE ID=$onum_id")->fetch_assoc();
    $onum    = (int)($orow['num'] ?? 1);
    $codenum = $ocode . '-' . sprintf('%04d', $onum);
    $ckey    = rand(1000, 9999);

    // invoice
    $db->query("INSERT INTO invoice SET
        checkoutID=$checkout_id, cartID=$cart_id,
        affiliateID=$aff_id, orderCode='$esc_code', orderNum=$onum,
        customerKey=$ckey, fulfilled='N',
        purchaseDate=Now(), installDate='$install_date'");
    $invoice_id = $db->insert_id;
    $db->close();

    if (!$invoice_id) { ivr_log('invoice insert failed'); return false; }

    // Write vehicle data to ivr_vehicles (before close)
    $v_year  = $db->real_escape_string($sess['year']    ?? '');
    $v_make  = $db->real_escape_string($sess['make']    ?? '');
    $v_model = $db->real_escape_string($sess['model']   ?? '');
    $v_style = $db->real_escape_string($sess['style']   ?? '');
    $v_glass = $db->real_escape_string($sess['glass']   ?? '');
    $v_acct  = $db->real_escape_string($acct['code']    ?? '');
    $db->query("INSERT INTO ivr_vehicles
        (invoice_id, cart_id, year, make, model, style, glass, zip, account, nags_part, nags_style)
        VALUES ($invoice_id, $cart_id, '$v_year', '$v_make', '$v_model',
                '$v_style', '$v_glass', '{$f['zip']}', '$v_acct',
                '$nags_part', '$nags_style')");

    $db->close();

    $sess['invoice_id']   = $invoice_id;
    $sess['order_num']    = $codenum;
    $sess['customer_key'] = $ckey;
    $sess['install_date'] = $install_date;

    ivr_log("Order created: $codenum invoiceID=$invoice_id");
    return true;
}

// ─── BUILD ORDER EMAIL ────────────────────────────────────────────────────────
function build_order_email(&$sess, $vin_url, $pay_url = '') {
    $acct    = $sess['account']      ?? array();
    $company = $acct['company']      ?? '';
    $onum    = $sess['order_num']    ?? '';
    $install = $sess['install_date'] ?? 'Tomorrow';
    $ani     = $sess['ani']          ?? '';
    $quoted  = isset($sess['quoted_price']) ? (float)$sess['quoted_price'] : 0;

    // Format ANI for display
    $ani_digits = preg_replace('/[^0-9]/', '', $ani);
    if (strlen($ani_digits) === 11 && $ani_digits[0] === '1') $ani_digits = substr($ani_digits, 1);
    $ani_display = strlen($ani_digits) === 10
        ? '(' . substr($ani_digits,0,3) . ') ' . substr($ani_digits,3,3) . '-' . substr($ani_digits,6)
        : $ani;

    return "
<html><body style='font-family:Arial,sans-serif;font-size:14px;'>
<h2 style='color:#003366;'>" . IVR_COMPANY . " - IVR Order</h2>
<table cellpadding='6' cellspacing='0' style='width:100%;max-width:620px;border-collapse:collapse;'>
  <tr><td colspan='2' style='background:#003366;color:#fff;padding:10px;font-weight:bold;font-size:16px;'>
    Order # $onum &nbsp;|&nbsp; $company
  </td></tr>
  <tr><td style='color:#666;width:35%;border-bottom:1px solid #eee;'>Install Date</td>
      <td style='border-bottom:1px solid #eee;'>$install <em style='color:#999;'>(default — subject to confirmation)</em></td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Vehicle</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['year'] ?? '') . ' ' . ($sess['make'] ?? '') . ' ' . ($sess['model'] ?? '') . ' ' . ($sess['style'] ?? '')) . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Glass</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['glass'] ?? '') . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Customer</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['first'] ?? '') . ' ' . ($sess['last'] ?? '')) . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Address</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars(($sess['street'] ?? '') . ', ' . ($sess['city'] ?? '') . ', ' . ($sess['state'] ?? '') . ' ' . ($sess['zip'] ?? '')) . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Phone (spoken)</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['phone'] ?? '') . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'><strong>Caller ID (CID)</strong></td>
      <td style='border-bottom:1px solid #eee;'><strong style='color:#003366;'>$ani_display</strong></td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>PO / RO</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['po'] ?? '') . "</td></tr>
  <tr><td style='color:#666;border-bottom:1px solid #eee;'>Unit #</td>
      <td style='border-bottom:1px solid #eee;'>" . htmlspecialchars($sess['unit'] ?? '') . "</td></tr>
  <tr bgcolor='#f9f9f9'><td style='color:#666;border-bottom:1px solid #eee;'>Notes</td>
      <td style='border-bottom:1px solid #eee;'>" . nl2br(htmlspecialchars($sess['notes'] ?? '')) . "</td></tr>" .
  ($quoted > 0 ? "
  <tr><td style='color:#666;border-bottom:1px solid #eee;'><strong>Quoted Price</strong></td>
      <td style='border-bottom:1px solid #eee;'><strong style='color:#003366;'>\$$quoted installed</strong></td></tr>" : "") . "
  <tr><td colspan='2' style='padding:14px 6px;'>
    <strong>VIN Entry Link:</strong><br>
    <a href='$vin_url' style='color:#003366;'>$vin_url</a>
  </td></tr>" .
  ($pay_url ? "
  <tr><td colspan='2' style='padding:0 6px 14px;'>
    <strong>Payment Link:</strong><br>
    <a href='$pay_url' style='color:#cc0000;font-weight:bold;'>$pay_url</a>
  </td></tr>" : "") . "
  <tr><td colspan='2' style='padding:6px;background:#fff8e1;font-size:12px;color:#888;'>
    Order placed via IVR &nbsp;|&nbsp; Caller ID: $ani_display &nbsp;|&nbsp; Twilio SID: " . htmlspecialchars($sess['call_sid'] ?? '') . "
  </td></tr>
</table>
</body></html>";
}

// ─── MAIN ────────────────────────────────────────────────────────────────────
header('Content-Type: text/xml');
validate_twilio();

// Get Twilio params
$call_sid = $_POST['CallSid']      ?? $_GET['CallSid']      ?? '';
$ani      = $_POST['From']         ?? $_GET['From']         ?? '';
$dnis     = $_POST['To']           ?? $_GET['To']           ?? '';
$speech   = $_POST['SpeechResult'] ?? $_GET['SpeechResult'] ?? '';
$digit    = $_POST['Digits']       ?? $_GET['Digits']       ?? '';
// Deepgram often appends a trailing period — strip it globally
$said     = trim(rtrim(trim($speech ?: $digit), '. '));
$action   = $_GET['action']        ?? 'welcome';
$sid      = $_GET['sid']           ?? $call_sid;

// Normalize DNIS — Twilio passes RC forwarded number
$dnis_clean = '+1' . preg_replace('/[^0-9]/', '', $dnis);
if (strlen($dnis_clean) > 12) $dnis_clean = substr($dnis_clean, 0, 12);

ivr_log("[$sid] action=$action dnis_raw=$dnis dnis_clean=$dnis_clean ani=$ani ForwardedFrom=" . ($_POST["ForwardedFrom"] ?? "none") . " said=" . json_encode($said));

// Load/init session
global $ACCOUNTS, $DEFAULT_ACCOUNT;
$sess = sess_load($sid);
if (empty($sess)) {
    $acct = $ACCOUNTS[$dnis_clean] ?? $DEFAULT_ACCOUNT;
    $sess = [
        'call_sid' => $call_sid,
        'ani'      => $ani,
        'dnis'     => $dnis_clean,
        'account'  => $acct,
    ];
    sess_save($sid, $sess);
}
$acct = $sess['account'] ?? $DEFAULT_ACCOUNT;

// ── Universal key presses ─────────────────────────────────────────────────────
if ($digit === '3') {
    sess_del($sid);
    twiml_transfer(IVR_TRANSFER);
}
if ($digit === '2') {
    sess_del($sid);
    $acct_new = isset($ACCOUNTS[$dnis_clean]) ? $ACCOUNTS[$dnis_clean] : $DEFAULT_ACCOUNT;
    $sess = array('call_sid' => $call_sid, 'ani' => $ani, 'dnis' => $dnis_clean, 'account' => $acct_new);
    sess_save($sid, $sess);
    twiml_redirect('welcome', $sid);
}
if (($digit === '4' || stripos($said, 'order') !== false) && $action !== 'got_price_response') {
    twiml_play_then_gather('joined/07_name.mp3', 'got_name', $sid, 8);
}

// ─── STEP ROUTER ─────────────────────────────────────────────────────────────
switch ($action) {

    // ── WELCOME + ZIP ────────────────────────────────────────────────────────
    case 'welcome':
        twiml_record('joined/01_welcome.mp3', 'got_zip', $sid, 6);

    // ── GOT ZIP ──────────────────────────────────────────────────────────────
    case 'got_zip':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';

        // Processing pass - transcribe ZIP
        if (!empty($sess['zip_processing'])) {
            unset($sess['zip_processing']);
            $rec = isset($sess['rec_zip']) ? $sess['rec_zip'] : $recording_url;
            $zip_transcript = whisper_transcribe($rec);
            ivr_log("[$sid] ZIP transcript: $zip_transcript");
            $zip = preg_replace('/[^0-9]/', '', $zip_transcript);
            // Handle spoken zip like "nine three one oh three"
            if (strlen($zip) !== 5) {
                $zip_words = array('zero'=>'0','one'=>'1','two'=>'2','three'=>'3','four'=>'4','five'=>'5','six'=>'6','seven'=>'7','eight'=>'8','nine'=>'9','oh'=>'0','o'=>'0');
                $spoken = strtolower($zip_transcript);
                $zip_digits = '';
                foreach (preg_split('/[\s,]+/', $spoken) as $word) {
                    if (isset($zip_words[$word])) $zip_digits .= $zip_words[$word];
                    elseif (is_numeric($word)) $zip_digits .= $word;
                }
                if (strlen($zip_digits) === 5) $zip = $zip_digits;
            }
            if (strlen($zip) !== 5) {
                twiml_record('joined/02_zip_retry.mp3', 'got_zip', $sid, 6);
            }
            $location = zip_to_city_state($zip);
            $sess['zip']   = $zip;
            $sess['city']  = $location['city'];
            $sess['state'] = $location['state'];
            ivr_log("[$sid] ZIP $zip → {$location['city']}, {$location['state']}");
            sess_save($sid, $sess);
            twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);
        }

        if (empty($recording_url)) {
            twiml_record('joined/01_welcome.mp3', 'got_zip', $sid, 6);
        }
        // Store recording and play zipwait while we process
        $sess['rec_zip'] = $recording_url;
        $sess['zip_processing'] = true;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_zip', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    case 'process_zip':
        twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);

    // ── GOT GLASS ────────────────────────────────────────────────────────────
    case 'got_glass':
        // Processing pass — check FIRST before looking for RecordingUrl
        if (!empty($sess['glass_processing'])) {
            unset($sess['glass_processing']);
            $recording_url = isset($sess['rec_glass']) ? $sess['rec_glass'] : '';
        } else {
            $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
            if (empty($recording_url)) {
                twiml_record('joined/03_ask_glass.mp3', 'got_glass', $sid, 3);
            }
            // Store and play filler
            $sess['rec_glass'] = $recording_url;
            $sess['glass_processing'] = true;
            sess_save($sid, $sess);
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
            echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_glass', $sid)) . '</Redirect>' . "\n";
            echo '</Response>' . "\n";
            exit;
        }
        $glass_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Glass transcript: $glass_transcript");

        if (empty(trim($glass_transcript))) {
            twiml_record('joined/03_1_ask_glass_retry.mp3', 'got_glass', $sid, 3);
        }
        // Route based on windshield or other
        if (stripos($glass_transcript, 'other') !== false) {
            twiml_record('joined/04_otherglass.mp3', 'got_other_glass', $sid, 8);
        }
        // "windshield" or anything else = windshield
        $sess['glass'] = 'Windshield';
        sess_save($sid, $sess);
        twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);

    // ── GOT OTHER GLASS ───────────────────────────────────────────────────────
    case 'got_other_glass':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/04_otherglass.mp3', 'got_other_glass', $sid, 8);
        }
        $other_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Other glass transcript: $other_transcript");
        if (empty($other_transcript)) {
            twiml_record('joined/04_1_otherglass_retry.mp3', 'got_other_glass', $sid, 8);
        }
        $other = strtolower($other_transcript);
        $glass_map = array(
            'left front'    => 'Driver Side',
            'driver'        => 'Driver Side',
            'left rear'     => 'Driver Rear',
            'right front'   => 'Passenger Side',
            'passenger'     => 'Passenger Side',
            'right rear'    => 'Passenger Rear',
            'back'          => 'Back Glass',
            'rear'          => 'Back Glass',
            'quarter'       => 'Quarter Glass',
        );
        $glass_normalized = 'Back Glass'; // default if other
        foreach ($glass_map as $k => $v) {
            if (strpos($other, $k) !== false) { $glass_normalized = $v; break; }
        }
        $sess['glass'] = $glass_normalized;
        sess_save($sid, $sess);
        twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);

    // ── GOT YEAR/MAKE/MODEL/STYLE ─────────────────────────────────────────────
    case 'got_yearmake':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/05_yearmake.mp3', 'got_yearmake', $sid, 12);
        }
        // Store recording URL in session and play wait prompt
        $sess['rec_yearmake'] = $recording_url;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('process_yearmake', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── PROCESS YEAR/MAKE/MODEL ───────────────────────────────────────────────
    case 'process_yearmake':
        $recording_url = isset($sess['rec_yearmake']) ? $sess['rec_yearmake'] : '';
        $transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] YMM transcript: $transcript");
        if (empty($transcript)) {
            twiml_record('joined/06_yearmake_retry.mp3', 'got_yearmake', $sid, 12);
        }
        $extracted = extract_vehicle($transcript);
        ivr_log("[$sid] YMM extract from '$transcript': " . json_encode($extracted));

        if (!empty($extracted['make']) && !empty($extracted['model'])) {
            $sess['year']  = !empty($extracted['year'])  ? $extracted['year']  : (isset($sess['year']) ? $sess['year'] : date('Y'));
            $sess['make']  = $extracted['make'];
            $sess['model'] = $extracted['model'];
            $style_raw = strtolower($transcript);
            if (strpos($style_raw, '2') !== false || strpos($style_raw, 'two') !== false) {
                $sess['style'] = '2 Door';
            } elseif (strpos($style_raw, '4') !== false || strpos($style_raw, 'four') !== false) {
                $sess['style'] = '4 Door';
            } else {
                $sess['style'] = '';
            }
            sess_save($sid, $sess);
            twiml_record('joined/07_name.mp3', 'got_name', $sid, 8);
        }
        twiml_record('joined/06_yearmake_retry.mp3', 'got_yearmake', $sid, 12);

    // ── GOT NAME ─────────────────────────────────────────────────────────────
    case 'got_name':
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/07_name.mp3', 'got_name', $sid, 8);
        }
        if (empty($sess['name_processing'])) {
            $sess['rec_name'] = $recording_url;
            $sess['name_processing'] = true;
            sess_save($sid, $sess);
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<Response>' . "\n";
            echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
            echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_name', $sid)) . '</Redirect>' . "\n";
            echo '</Response>' . "\n";
            exit;
        }
        unset($sess['name_processing']);
        $recording_url = isset($sess['rec_name']) ? $sess['rec_name'] : $recording_url;
        $name_transcript = whisper_transcribe($recording_url);
        ivr_log("[$sid] Name transcript: $name_transcript");
        if (empty($name_transcript)) {
            twiml_record('joined/08_name_retry.mp3', 'got_name', $sid, 8);
        }
        // Strip common greeting phrases
        $name_lower = strtolower(trim($name_transcript));
        foreach (array('my name is', 'it\'s', 'this is', 'the name is') as $g) {
            $name_lower = trim(preg_replace('/^' . preg_quote($g, '/') . '\s*/i', '', $name_lower));
        }
        $parts = explode(' ', $name_lower, 2);
        $sess['first'] = ucfirst($parts[0] ?? '');
        $sess['last']  = ucwords($parts[1] ?? '');
        if (empty($sess['first'])) {
            twiml_record('joined/08_name_retry.mp3', 'got_name', $sid, 8);
        }
        sess_save($sid, $sess);
        twiml_redirect('got_street', $sid);

    // ── GOT STREET (recording → Whisper → address validation) ────────────────
    case 'got_street':
        // Processing pass — triggered after wait prompt
        if (!empty($sess['street_processing'])) {
            unset($sess['street_processing']);
            $rec = isset($sess['rec_street']) ? $sess['rec_street'] : '';
            if (empty($rec)) {
                twiml_record('joined/09_street.mp3', 'got_street', $sid, 15);
            }
            $street_transcript = whisper_transcribe($rec);
            ivr_log("[$sid] Street transcript: $street_transcript");
            if (empty($street_transcript)) {
                sess_save($sid, $sess);
                twiml_record('joined/10_street_retry.mp3', 'got_street', $sid, 15);
            }
            $street = ucwords(strtolower(trim($street_transcript)));
            // Address validation via Nominatim
            $zip     = isset($sess['zip']) ? $sess['zip'] : '';
            $query   = urlencode($street . ' ' . $zip . ' USA');
            $nom_url = 'https://nominatim.openstreetmap.org/search?q=' . $query . '&format=json&limit=1&addressdetails=1';
            $ch = curl_init($nom_url);
            curl_setopt_array($ch, array(
                CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_USERAGENT      => 'W2G-IVR/1.0 (w2gusa@gmail.com)',
            ));
            $nom_data = json_decode(curl_exec($ch), true);
            curl_close($ch);
            if (!empty($nom_data[0]['address'])) {
                $addr = $nom_data[0]['address'];
                $h = isset($addr['house_number']) ? $addr['house_number'] : '';
                $r = isset($addr['road'])         ? $addr['road']         : '';
                if ($h && $r) {
                    $validated = $h . ' ' . $r;
                    ivr_log("[$sid] Address validated: '$street' → '$validated'");
                    $street = $validated;
                }
            }
            $sess['street'] = $street;
            sess_save($sid, $sess);
            if (!empty($acct['ask_po'])) {
                twiml_gather('joined/11_PO.mp3', 'got_po', $sid, 6);
            } else {
                twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);
            }
        }

        // First pass — check for recording URL
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/09_street.mp3', 'got_street', $sid, 15);
        }

        // Recording received — store in session, set processing flag, play wait
        $sess['rec_street'] = $recording_url;
        $sess['street_processing'] = true;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_street', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── GOT PO ───────────────────────────────────────────────────────────────
    case 'got_po':
        $po = strtoupper(trim($said));
        $skip_po = (stripos($po, 'SKIP') !== false || stripos($po, 'PASS') !== false || empty($po));
        if ($skip_po) $po = '';
        if (!$skip_po && !empty($acct['po_prefill']) && empty($po)) $po = $acct['po_prefill'];
        if (empty($po) && !$skip_po) {
            twiml_gather('joined/12_PO_retry.mp3', 'got_po', $sid, 6);
        }
        $sess['po'] = $po;
        sess_save($sid, $sess);
        twiml_gather('joined/13_Unit.mp3', 'got_unit', $sid, 6);

    // ── GOT UNIT ─────────────────────────────────────────────────────────────
    case 'got_unit':
        $unit = strtoupper(trim($said));
        $skip_unit = (stripos($unit, 'SKIP') !== false || stripos($unit, 'PASS') !== false || empty($unit));
        if ($skip_unit) $unit = '';
        if (empty($unit) && !$skip_unit) {
            twiml_gather('joined/14_Unit_replay.mp3', 'got_unit', $sid, 6);
        }
        $sess['unit'] = $unit;
        sess_save($sid, $sess);
        twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);

    // ── GOT CONTACT (recording → Whisper → Claude extract) ───────────────────
    case 'got_contact':
        // Processing pass
        if (!empty($sess['contact_processing'])) {
            unset($sess['contact_processing']);
            $rec = isset($sess['rec_contact']) ? $sess['rec_contact'] : '';
            if (empty($rec)) {
                sess_save($sid, $sess);
                twiml_record('joined/16_contact_retry.mp3', 'got_contact', $sid, 30);
            }
            $contact_transcript = whisper_transcribe($rec);
            ivr_log("[$sid] Contact transcript: $contact_transcript");
            if (empty($contact_transcript)) {
                sess_save($sid, $sess);
                twiml_record('joined/16_contact_retry.mp3', 'got_contact', $sid, 30);
            }
            $contact_fields = claude_extract_order($contact_transcript, $acct);
            $email = isset($contact_fields['email']) ? $contact_fields['email'] : '';
            $phone = isset($contact_fields['phone']) ? $contact_fields['phone'] : '';
            if (empty($phone)) {
                $digits = preg_replace('/[^0-9]/', '', $contact_transcript);
                if (strlen($digits) >= 10) $phone = substr($digits, -10);
            }
            if (empty($email)) {
                $email_raw = preg_replace('/\s+at\s+/i', '@', strtolower($contact_transcript));
                $email_raw = preg_replace('/\s+dot\s+/i', '.', $email_raw);
                $email_raw = preg_replace('/\s+/', '', $email_raw);
                if (strpos($email_raw, '@') !== false) $email = $email_raw;
            }
            $sess['email'] = $email;
            $sess['phone'] = $phone;
            sess_save($sid, $sess);
            twiml_gather('joined/19_notes_prompt.mp3', 'got_notes_final', $sid, 8);
        }

        // First pass
        $recording_url = isset($_POST['RecordingUrl']) ? $_POST['RecordingUrl'] : '';
        if (empty($recording_url)) {
            twiml_record('joined/15_contact.mp3', 'got_contact', $sid, 30);
        }
        $sess['rec_contact'] = $recording_url;
        $sess['contact_processing'] = true;
        sess_save($sid, $sess);
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/silence_filler.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('got_contact', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── GOT NOTES/ADDITIONAL CELL ─────────────────────────────────────────────
    case 'got_notes_final':
        $notes_raw = trim($said);
        $skip = (stripos($notes_raw, 'skip') !== false || empty($notes_raw));

        if (!$skip) {
            // Check if they provided a cell phone number
            $extra_digits = preg_replace('/[^0-9]/', '', $notes_raw);
            if (strlen($extra_digits) >= 10) {
                $sess['sms_phone'] = substr($extra_digits, -10);
                $sess['phone_is_cell'] = true;
                ivr_log("[$sid] Cell phone from notes: {$sess['sms_phone']}");
            }
            // Store notes (strip just the phone number part if that's all they said)
            $notes_clean = trim(preg_replace('/[\d\s\-\(\)\.]+/', ' ', $notes_raw));
            if (strlen($notes_clean) > 2) {
                $sess['notes'] = $notes_raw;
            }
        }
        sess_save($sid, $sess);

        // Play thank you then process
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<Response>' . "\n";
        echo '  <Play>' . PROMPT_BASE . 'joined/20_fleet_thanks.mp3</Play>' . "\n";
        echo '  <Redirect method="POST">' . htmlspecialchars(next_url('process_joined', $sid)) . '</Redirect>' . "\n";
        echo '</Response>' . "\n";
        exit;

    // ── PROCESS JOINED ORDER ──────────────────────────────────────────────────
    case 'process_joined':
        // NAGS lookup
        // For commercial trucks, door count doesn't map to NAGS style
        // Pass empty style so NAGS auto-selects the only available option
        $truck_makes = array('freightliner','kenworth','peterbilt','international','mack','hino','isuzu','volvo','sterling');
        $is_truck = false;
        foreach ($truck_makes as $tm) {
            if (stripos($sess['make'] ?? '', $tm) !== false) { $is_truck = true; break; }
        }
        $nags_style = $is_truck ? '' : (isset($sess['style']) ? $sess['style'] : '');
        $nags_model = isset($sess['model']) ? $sess['model'] : '';

        // NAGS model aliases — try alternate model names if primary fails
        $model_aliases = array(
            'M2'        => array('Business Class M2', 'Business Class'),
            'Cascadia'  => array('Cascadia 113', 'Cascadia 125'),
            'ProStar'   => array('ProStar+'),
            'LoneStar'  => array('LoneStar'),
        );

        $nags = lookup_nags($sess['year'] ?? '', $sess['make'] ?? '', $nags_model, $nags_style, $sess['glass'] ?? 'Windshield', $sess['zip'] ?? '90001');

        // If no part found and model has aliases, try them
        if (empty($nags['part']) && isset($model_aliases[$nags_model])) {
            foreach ($model_aliases[$nags_model] as $alias) {
                ivr_log("[$sid] NAGS alias try: $nags_model → $alias");
                $nags = lookup_nags($sess['year'] ?? '', $sess['make'] ?? '', $alias, $nags_style, $sess['glass'] ?? 'Windshield', $sess['zip'] ?? '90001');
                if (!empty($nags['part'])) break;
            }
        }
        if (!empty($nags['part'])) {
            $sess['nags_part']  = $nags['part'];
            $sess['nags_style'] = $nags['style'];
        } else {
            $sess['nags_part']  = '';
            $sess['nags_style'] = $sess['style'] ?? '';
        }
        sess_save($sid, $sess);

        $ok = write_order($sess);
        sess_save($sid, $sess);

        if ($ok) {
            $invoice_id = $sess['invoice_id']   ?? 0;
            $order_num  = $sess['order_num']    ?? '';
            $ckey       = $sess['customer_key'] ?? '';
            $needs_pay  = !empty($acct['requires_payment']);
            $vin_url    = VIN_LINK_BASE . '?order=' . $invoice_id . '&key=' . $ckey;
            $pay_url    = PAY_LINK_BASE . '?order=' . $invoice_id . '&key=' . $ckey;

            send_email(W2G_EMAIL,
                "IVR Order $order_num - " . ($acct['company'] ?? '') . " - {$sess['year']} {$sess['make']} {$sess['model']}",
                build_order_email($sess, $vin_url, $needs_pay ? $pay_url : ''));

            // SMS — send to provided cell number or ANI
            $sms_to = !empty($sess['sms_phone']) ? '+1' . $sess['sms_phone'] : (isset($sess['ani']) ? $sess['ani'] : '');
            if ($sms_to) {
                $sms = IVR_COMPANY . ": Order $order_num placed. Enter VIN: $vin_url";
                if ($needs_pay) $sms .= " Pay: $pay_url";
                $sms_sent = rc_sms($sms_to, $sms);
                ivr_log("[$sid] SMS to $sms_to: " . ($sms_sent ? 'OK' : 'FAILED'));
            }
            ivr_log("[$sid] Joined order created: $order_num");
        }

        sess_del($sid);
        twiml_hangup();


    default:
        twiml_transfer(IVR_TRANSFER);
}
