<?php
/**
 * Quote business logic service.
 *
 * Handles all database operations for quotes: create, read, update, submit,
 * item management, totals calculation, activity logging, and admin functions.
 *
 * All functions accept a PDO instance and return associative arrays.
 */

// Deny direct access
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
    http_response_code(403);
    exit('Direct access denied.');
}

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../db.php';

// --------------------------------------------------------------------------
// Constants
// --------------------------------------------------------------------------

define('VALID_STATUSES', [
    'draft', 'submitted', 'viewed', 'followed_up', 'converted', 'expired', 'archived'
]);

define('VALID_CATEGORIES', [
    'gps_monitoring', 'support_plan', 'voip', 'web_hosting', 'email',
    'hardware', 'addon', 'backup', 'security', 'other'
]);

define('VALID_BILLING_FREQUENCIES', ['monthly', 'yearly', 'one_time']);

define('SUBMITTED_STATUSES', ['submitted', 'viewed', 'followed_up', 'converted']);

// --------------------------------------------------------------------------
// Table name helpers
// --------------------------------------------------------------------------

function tbl_quotes(): string    { return DB_TABLE_PREFIX . 'quotes'; }
function tbl_items(): string     { return DB_TABLE_PREFIX . 'quote_items'; }
function tbl_activity(): string  { return DB_TABLE_PREFIX . 'quote_activity'; }
function tbl_notif(): string     { return DB_TABLE_PREFIX . 'quote_notifications'; }

// --------------------------------------------------------------------------
// Activity logging
// --------------------------------------------------------------------------

/**
 * Log an activity record for a quote.
 *
 * @param PDO         $db        Database connection.
 * @param string      $quote_id  UUID of the quote.
 * @param string      $action    Action name (created, updated, submitted, etc.).
 * @param string|null $details   Freeform details (stored as JSON wrapper for CHECK compat).
 * @param string|null $step_name Wizard step name.
 * @param string|null $ip        Client IP address.
 */
function log_activity(
    PDO $db,
    string $quote_id,
    string $action,
    ?string $details = null,
    ?string $step_name = null,
    ?string $ip = null
): void {
    $id = generate_uuid();
    $details_json = $details !== null ? json_encode(['message' => $details]) : null;

    $sql = sprintf(
        'INSERT INTO %s (id, quote_id, action, step_name, details, ip_address, created_at)
         VALUES (:id, :quote_id, :action, :step_name, :details, :ip, :created_at)',
        tbl_activity()
    );

    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':id'        => $id,
        ':quote_id'  => $quote_id,
        ':action'    => $action,
        ':step_name' => $step_name,
        ':details'   => $details_json,
        ':ip'        => $ip,
        ':created_at' => utc_now(),
    ]);
}

// --------------------------------------------------------------------------
// Totals calculation
// --------------------------------------------------------------------------

/**
 * Calculate monthly and setup totals from a list of item rows.
 *
 * Monthly calculation:
 *   - monthly items: unit_price * quantity
 *   - yearly items:  unit_price * quantity / 12
 *   - one_time items: 0
 *
 * Setup total: sum of all setup_price values.
 *
 * @param array $items Array of item associative arrays.
 * @return array{monthly: string, setup: string} Formatted totals with 2 decimal places.
 */
function calculate_totals(array $items): array
{
    $monthly = '0';
    $setup   = '0';

    foreach ($items as $item) {
        $unit_price = $item['unit_price'] ?? '0';
        $qty        = (int)($item['quantity'] ?? 1);
        $setup_price = $item['setup_price'] ?? '0';
        $freq       = $item['billing_frequency'] ?? 'monthly';

        $line_total = bcmul($unit_price, (string)$qty, 4);

        if ($freq === 'monthly') {
            $monthly = bcadd($monthly, $line_total, 4);
        } elseif ($freq === 'yearly') {
            $monthly_portion = bcdiv($line_total, '12', 4);
            $monthly = bcadd($monthly, $monthly_portion, 4);
        }
        // one_time items contribute 0 to monthly

        $setup = bcadd($setup, $setup_price, 4);
    }

    return [
        'monthly' => number_format((float)$monthly, 2, '.', ''),
        'setup'   => number_format((float)$setup, 2, '.', ''),
    ];
}

/**
 * Recalculate and persist totals for a quote by summing its items.
 *
 * @param PDO    $db       Database connection.
 * @param string $quote_id UUID of the quote.
 */
function recalculate_totals(PDO $db, string $quote_id): void
{
    $items = fetch_items_for_quote($db, $quote_id);
    $totals = calculate_totals($items);

    $sql = sprintf(
        'UPDATE %s SET monthly_total = :monthly, setup_total = :setup, updated_at = :now WHERE id = :id',
        tbl_quotes()
    );
    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':monthly' => $totals['monthly'],
        ':setup'   => $totals['setup'],
        ':now'     => utc_now(),
        ':id'      => $quote_id,
    ]);
}

// --------------------------------------------------------------------------
// Item helpers
// --------------------------------------------------------------------------

/**
 * Fetch all items for a given quote.
 *
 * @param PDO    $db       Database connection.
 * @param string $quote_id UUID of the quote.
 * @return array List of item rows.
 */
function fetch_items_for_quote(PDO $db, string $quote_id): array
{
    $sql = sprintf('SELECT * FROM %s WHERE quote_id = :qid ORDER BY created_at ASC', tbl_items());
    $stmt = $db->prepare($sql);
    $stmt->execute([':qid' => $quote_id]);
    return $stmt->fetchAll();
}

/**
 * Insert a single item row.
 *
 * @param PDO    $db       Database connection.
 * @param string $quote_id UUID of the parent quote.
 * @param array  $item     Item data with keys matching the schema.
 * @return string The generated item UUID.
 */
function insert_item(PDO $db, string $quote_id, array $item): string
{
    $id = generate_uuid();

    $sql = sprintf(
        'INSERT INTO %s
            (id, quote_id, category, product_code, product_name, description,
             quantity, unit_price, setup_price, billing_frequency, tier, is_recommended, created_at)
         VALUES
            (:id, :qid, :cat, :code, :name, :desc, :qty, :price, :setup, :freq, :tier, :rec, :created_at)',
        tbl_items()
    );

    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':id'    => $id,
        ':qid'   => $quote_id,
        ':cat'   => $item['category'] ?? 'other',
        ':code'  => $item['product_code'] ?? '',
        ':name'  => $item['product_name'] ?? '',
        ':desc'  => $item['description'] ?? null,
        ':qty'   => (int)($item['quantity'] ?? 1),
        ':price' => $item['unit_price'] ?? '0.00',
        ':setup' => $item['setup_price'] ?? '0.00',
        ':freq'  => $item['billing_frequency'] ?? 'monthly',
        ':tier'  => $item['tier'] ?? null,
        ':rec'   => (int)($item['is_recommended'] ?? 0),
        ':created_at' => utc_now(),
    ]);

    return $id;
}

/**
 * Delete all items for a quote.
 *
 * @param PDO    $db       Database connection.
 * @param string $quote_id UUID of the quote.
 */
function delete_all_items(PDO $db, string $quote_id): void
{
    $sql = sprintf('DELETE FROM %s WHERE quote_id = :qid', tbl_items());
    $stmt = $db->prepare($sql);
    $stmt->execute([':qid' => $quote_id]);
}

// --------------------------------------------------------------------------
// Quote CRUD
// --------------------------------------------------------------------------

/**
 * Create a new quote draft.
 *
 * Generates a UUID and access token, inserts the quote row, optionally
 * inserts initial items, calculates totals, and logs a "created" activity.
 *
 * @param PDO         $db   Database connection.
 * @param array       $data Request body data (employee_count, items).
 * @param string|null $ip   Client IP.
 * @param string|null $ua   User-Agent.
 * @return array The created quote row (without items).
 */
function create_quote(PDO $db, array $data, ?string $ip = null, ?string $ua = null): array
{
    $id    = generate_uuid();
    $token = generate_access_token();
    $now   = utc_now();
    $expires = gmdate('Y-m-d H:i:s', strtotime("+".QUOTE_DRAFT_EXPIRY_DAYS." days"));

    $employee_count = isset($data['employee_count']) ? (int)$data['employee_count'] : null;

    $sql = sprintf(
        'INSERT INTO %s
            (id, access_token, status, employee_count, ip_address, user_agent,
             monthly_total, setup_total, expires_at, created_at, updated_at)
         VALUES
            (:id, :token, :status, :emp, :ip, :ua, :mt, :st, :exp, :now, :now2)',
        tbl_quotes()
    );

    try {
        $db->beginTransaction();

        $stmt = $db->prepare($sql);
        $stmt->execute([
            ':id'     => $id,
            ':token'  => $token,
            ':status' => 'draft',
            ':emp'    => $employee_count,
            ':ip'     => $ip,
            ':ua'     => $ua,
            ':mt'     => '0.00',
            ':st'     => '0.00',
            ':exp'    => $expires,
            ':now'    => $now,
            ':now2'   => $now,
        ]);

        // Insert initial items if provided
        if (!empty($data['items']) && is_array($data['items'])) {
            foreach ($data['items'] as $item) {
                insert_item($db, $id, $item);
            }
            recalculate_totals($db, $id);
        }

        // Log activity
        log_activity(
            $db, $id, 'created',
            "Quote draft created, employee_count={$employee_count}",
            null, $ip
        );

        $db->commit();

    } catch (PDOException $e) {
        $db->rollBack();
        // If token collision, retry once
        if (strpos($e->getMessage(), 'access_token') !== false) {
            app_log('WARNING', 'Access token collision, retrying create_quote');
            return create_quote($db, $data, $ip, $ua);
        }
        app_log('ERROR', 'Failed to create quote: ' . $e->getMessage());
        error_response('Failed to create quote: ' . $e->getMessage(), 500);
    }

    return fetch_quote_row($db, $id);
}

/**
 * Fetch a raw quote row by ID.
 *
 * @param PDO    $db Database connection.
 * @param string $id Quote UUID.
 * @return array|null Quote row or null.
 */
function fetch_quote_row(PDO $db, string $id): ?array
{
    $sql = sprintf('SELECT * FROM %s WHERE id = :id', tbl_quotes());
    $stmt = $db->prepare($sql);
    $stmt->execute([':id' => $id]);
    $row = $stmt->fetch();
    return $row ?: null;
}

/**
 * Fetch a quote by access token, with expiration check.
 *
 * If the quote is a draft and expired, its status is updated to "expired".
 *
 * @param PDO    $db    Database connection.
 * @param string $token Access token.
 * @return array Quote row.
 */
function get_quote_by_token(PDO $db, string $token): array
{
    $sql = sprintf('SELECT * FROM %s WHERE access_token = :token', tbl_quotes());
    $stmt = $db->prepare($sql);
    $stmt->execute([':token' => $token]);
    $quote = $stmt->fetch();

    if (!$quote) {
        error_response('Quote not found', 404);
    }

    // Check expiration for draft quotes
    if (
        $quote['status'] === 'draft'
        && $quote['expires_at'] !== null
        && strtotime($quote['expires_at']) < time()
    ) {
        $upd = sprintf('UPDATE %s SET status = :s, updated_at = :now WHERE id = :id', tbl_quotes());
        $stmt2 = $db->prepare($upd);
        $stmt2->execute([':s' => 'expired', ':now' => utc_now(), ':id' => $quote['id']]);
        $quote['status'] = 'expired';
    }

    return $quote;
}

/**
 * Fetch a quote by its UUID (admin access).
 *
 * @param PDO    $db Database connection.
 * @param string $id Quote UUID.
 * @return array Quote row.
 */
function get_quote_by_id(PDO $db, string $id): array
{
    $quote = fetch_quote_row($db, $id);
    if (!$quote) {
        error_response("Quote with ID {$id} not found", 404);
    }
    return $quote;
}

/**
 * Build the full public quote response (quote + items, with computed fields).
 *
 * @param PDO    $db    Database connection.
 * @param array  $quote Raw quote row.
 * @return array Response-ready associative array.
 */
function build_quote_response(PDO $db, array $quote): array
{
    $items_raw = fetch_items_for_quote($db, $quote['id']);
    $items = array_map('format_item_response', $items_raw);

    return [
        'id'             => $quote['id'],
        'access_token'   => $quote['access_token'],
        'status'         => $quote['status'],
        'company_name'   => $quote['company_name'],
        'contact_name'   => $quote['contact_name'],
        'contact_email'  => $quote['contact_email'],
        'contact_phone'  => $quote['contact_phone'],
        'employee_count' => $quote['employee_count'] !== null ? (int)$quote['employee_count'] : null,
        'monthly_total'  => number_format((float)$quote['monthly_total'], 2, '.', ''),
        'setup_total'    => number_format((float)$quote['setup_total'], 2, '.', ''),
        'expires_at'     => format_datetime($quote['expires_at']),
        'submitted_at'   => format_datetime($quote['submitted_at']),
        'created_at'     => format_datetime($quote['created_at']),
        'updated_at'     => format_datetime($quote['updated_at']),
        'items'          => $items,
    ];
}

/**
 * Build the full admin quote response (quote + items + activities + notifications).
 *
 * @param PDO   $db    Database connection.
 * @param array $quote Raw quote row.
 * @return array Response-ready associative array.
 */
function build_admin_quote_response(PDO $db, array $quote): array
{
    $base = build_quote_response($db, $quote);

    // Add admin-only fields
    $base['ip_address'] = $quote['ip_address'];
    $base['user_agent'] = $quote['user_agent'];

    // Fetch activities
    $sql_act = sprintf(
        'SELECT * FROM %s WHERE quote_id = :qid ORDER BY created_at DESC',
        tbl_activity()
    );
    $stmt = $db->prepare($sql_act);
    $stmt->execute([':qid' => $quote['id']]);
    $activities_raw = $stmt->fetchAll();

    $base['activities'] = array_map(function ($a) {
        return [
            'id'         => $a['id'],
            'quote_id'   => $a['quote_id'],
            'action'     => $a['action'],
            'step_name'  => $a['step_name'],
            'details'    => $a['details'],
            'ip_address' => $a['ip_address'],
            'created_at' => format_datetime($a['created_at']),
        ];
    }, $activities_raw);

    // Fetch notifications
    $sql_notif = sprintf(
        'SELECT * FROM %s WHERE quote_id = :qid ORDER BY created_at DESC',
        tbl_notif()
    );
    $stmt2 = $db->prepare($sql_notif);
    $stmt2->execute([':qid' => $quote['id']]);
    $notifs_raw = $stmt2->fetchAll();

    $base['notifications'] = array_map(function ($n) {
        return [
            'id'               => $n['id'],
            'quote_id'         => $n['quote_id'],
            'notification_type' => $n['notification_type'],
            'recipient'        => $n['recipient'],
            'subject'          => $n['subject'],
            'status'           => $n['status'],
            'attempts'         => (int)$n['attempts'],
            'last_attempt_at'  => format_datetime($n['last_attempt_at']),
            'sent_at'          => format_datetime($n['sent_at']),
            'error_message'    => $n['error_message'],
            'created_at'       => format_datetime($n['created_at']),
        ];
    }, $notifs_raw);

    return $base;
}

/**
 * Format a raw item row into the API response shape with computed fields.
 *
 * @param array $item Raw item row from database.
 * @return array Formatted item response.
 */
function format_item_response(array $item): array
{
    $unit_price = $item['unit_price'] ?? '0.00';
    $qty        = (int)($item['quantity'] ?? 1);
    $freq       = $item['billing_frequency'] ?? 'monthly';
    $setup      = $item['setup_price'] ?? '0.00';

    $line_total = bcmul($unit_price, (string)$qty, 2);

    if ($freq === 'monthly') {
        $monthly_amount = $line_total;
    } elseif ($freq === 'yearly') {
        $monthly_amount = bcdiv($line_total, '12', 2);
    } else {
        $monthly_amount = '0.00';
    }

    return [
        'id'                => $item['id'],
        'quote_id'          => $item['quote_id'],
        'category'          => $item['category'],
        'product_code'      => $item['product_code'],
        'product_name'      => $item['product_name'],
        'description'       => $item['description'],
        'quantity'          => $qty,
        'unit_price'        => number_format((float)$unit_price, 2, '.', ''),
        'setup_price'       => number_format((float)$setup, 2, '.', ''),
        'billing_frequency' => $freq,
        'tier'              => $item['tier'],
        'is_recommended'    => (bool)$item['is_recommended'],
        'line_total'        => number_format((float)$line_total, 2, '.', ''),
        'monthly_amount'    => number_format((float)$monthly_amount, 2, '.', ''),
        'created_at'        => format_datetime($item['created_at']),
    ];
}

// --------------------------------------------------------------------------
// Update quote
// --------------------------------------------------------------------------

/**
 * Update a draft quote's fields and optionally replace all items.
 *
 * Only quotes with status "draft" can be updated. If items are provided
 * in the data, all existing items are deleted and replaced.
 *
 * @param PDO         $db    Database connection.
 * @param string      $token Access token.
 * @param array       $data  Update data.
 * @param string|null $ip    Client IP.
 * @return array Updated quote row.
 */
function update_quote(PDO $db, string $token, array $data, ?string $ip = null): array
{
    $quote = get_quote_by_token($db, $token);

    if ($quote['status'] !== 'draft') {
        error_response(
            "Cannot update quote with status '{$quote['status']}'. Only drafts can be modified.",
            400
        );
    }

    try {
        $db->beginTransaction();

        $changes = [];
        $updatable_fields = ['company_name', 'contact_name', 'contact_email', 'contact_phone', 'employee_count'];
        $sets = [];
        $params = [':id' => $quote['id']];

        foreach ($updatable_fields as $field) {
            if (array_key_exists($field, $data)) {
                $old = $quote[$field];
                $new = $data[$field];
                if ($old !== $new) {
                    $sets[] = "{$field} = :{$field}";
                    $params[":{$field}"] = $new;
                    $changes[] = "{$field}: {$old} -> {$new}";
                }
            }
        }

        if (!empty($sets)) {
            $sets[] = 'updated_at = :now';
            $params[':now'] = utc_now();
            $sql = sprintf('UPDATE %s SET %s WHERE id = :id', tbl_quotes(), implode(', ', $sets));
            $stmt = $db->prepare($sql);
            $stmt->execute($params);
        }

        // Replace items if provided
        if (array_key_exists('items', $data) && $data['items'] !== null) {
            delete_all_items($db, $quote['id']);
            $item_count = 0;
            if (is_array($data['items'])) {
                foreach ($data['items'] as $item) {
                    insert_item($db, $quote['id'], $item);
                    $item_count++;
                }
            }
            $changes[] = "items: replaced with {$item_count} items";
        }

        // Recalculate totals
        recalculate_totals($db, $quote['id']);

        // Log activity
        if (!empty($changes)) {
            log_activity(
                $db, $quote['id'], 'updated',
                'Quote updated: ' . implode(', ', $changes),
                null, $ip
            );
        }

        $db->commit();

    } catch (PDOException $e) {
        $db->rollBack();
        app_log('ERROR', 'Failed to update quote: ' . $e->getMessage());
        error_response('Failed to update quote: ' . $e->getMessage(), 500);
    }

    return get_quote_by_token($db, $token);
}

// --------------------------------------------------------------------------
// Add / remove items
// --------------------------------------------------------------------------

/**
 * Add a single item to a draft quote.
 *
 * @param PDO         $db    Database connection.
 * @param string      $token Access token.
 * @param array       $item  Item data.
 * @param string|null $ip    Client IP.
 * @return array Updated quote row.
 */
function add_item(PDO $db, string $token, array $item, ?string $ip = null): array
{
    $quote = get_quote_by_token($db, $token);

    if ($quote['status'] !== 'draft') {
        error_response('Cannot add items to a non-draft quote', 400);
    }

    try {
        $db->beginTransaction();

        insert_item($db, $quote['id'], $item);
        recalculate_totals($db, $quote['id']);

        log_activity(
            $db, $quote['id'], 'item_added',
            'Added item: ' . ($item['product_name'] ?? 'unknown'),
            null, $ip
        );

        $db->commit();

    } catch (PDOException $e) {
        $db->rollBack();
        app_log('ERROR', 'Failed to add item: ' . $e->getMessage());
        error_response('Failed to add item: ' . $e->getMessage(), 500);
    }

    return get_quote_by_token($db, $token);
}

/**
 * Remove a single item from a draft quote.
 *
 * @param PDO         $db      Database connection.
 * @param string      $token   Access token.
 * @param string      $item_id UUID of the item to remove.
 * @param string|null $ip      Client IP.
 * @return array Updated quote row.
 */
function remove_item(PDO $db, string $token, string $item_id, ?string $ip = null): array
{
    $quote = get_quote_by_token($db, $token);

    if ($quote['status'] !== 'draft') {
        error_response('Cannot remove items from a non-draft quote', 400);
    }

    // Find the item
    $sql = sprintf(
        'SELECT * FROM %s WHERE id = :iid AND quote_id = :qid',
        tbl_items()
    );
    $stmt = $db->prepare($sql);
    $stmt->execute([':iid' => $item_id, ':qid' => $quote['id']]);
    $item = $stmt->fetch();

    if (!$item) {
        error_response("Item with ID {$item_id} not found in this quote", 404);
    }

    try {
        $db->beginTransaction();

        $del = sprintf('DELETE FROM %s WHERE id = :iid', tbl_items());
        $stmt2 = $db->prepare($del);
        $stmt2->execute([':iid' => $item_id]);

        recalculate_totals($db, $quote['id']);

        log_activity(
            $db, $quote['id'], 'item_removed',
            'Removed item: ' . $item['product_name'],
            null, $ip
        );

        $db->commit();

    } catch (PDOException $e) {
        $db->rollBack();
        app_log('ERROR', 'Failed to remove item: ' . $e->getMessage());
        error_response('Failed to remove item: ' . $e->getMessage(), 500);
    }

    return get_quote_by_token($db, $token);
}

// --------------------------------------------------------------------------
// Submit quote
// --------------------------------------------------------------------------

/**
 * Submit a draft quote with contact information.
 *
 * Validates the quote is a draft and has items, updates contact fields,
 * sets status to "submitted", extends expiration to 90 days, and creates
 * a notification record.
 *
 * @param PDO         $db    Database connection.
 * @param string      $token Access token.
 * @param array       $data  Submission data (company_name, contact_name, contact_email, etc.).
 * @param string|null $ip    Client IP.
 * @return array The submitted quote row.
 */
function submit_quote(PDO $db, string $token, array $data, ?string $ip = null): array
{
    $quote = get_quote_by_token($db, $token);

    if ($quote['status'] !== 'draft') {
        error_response(
            "Cannot submit quote with status '{$quote['status']}'. Only drafts can be submitted.",
            400
        );
    }

    // Must have at least one item
    $items = fetch_items_for_quote($db, $quote['id']);
    if (empty($items)) {
        error_response(
            'Cannot submit quote without any items. Please add at least one service.',
            400
        );
    }

    $now = utc_now();
    $expires = gmdate('Y-m-d H:i:s', strtotime("+" . QUOTE_SUBMITTED_EXPIRY_DAYS . " days"));

    try {
        $db->beginTransaction();

        // Update contact info and status
        $sql = sprintf(
            'UPDATE %s SET
                company_name   = :company,
                contact_name   = :name,
                contact_email  = :email,
                contact_phone  = :phone,
                status         = :status,
                submitted_at   = :submitted,
                expires_at     = :expires,
                updated_at     = :now
             WHERE id = :id',
            tbl_quotes()
        );

        $stmt = $db->prepare($sql);
        $stmt->execute([
            ':company'   => trim($data['company_name'] ?? ''),
            ':name'      => trim($data['contact_name'] ?? ''),
            ':email'     => $data['contact_email'] ?? '',
            ':phone'     => $data['contact_phone'] ?? null,
            ':status'    => 'submitted',
            ':submitted' => $now,
            ':expires'   => $expires,
            ':now'       => $now,
            ':id'        => $quote['id'],
        ]);

        // Refresh the quote row to get updated totals
        $refreshed = fetch_quote_row($db, $quote['id']);

        // Log activity
        $contact_name  = trim($data['contact_name'] ?? '');
        $contact_email = $data['contact_email'] ?? '';
        $company_name  = trim($data['company_name'] ?? '');

        log_activity(
            $db, $quote['id'], 'submitted',
            "Quote submitted by {$contact_name} ({$contact_email}), company={$company_name}, monthly=\${$refreshed['monthly_total']}, setup=\${$refreshed['setup_total']}",
            null, $ip
        );

        // Create notification record
        $notif_id = generate_uuid();
        $sql_notif = sprintf(
            'INSERT INTO %s
                (id, quote_id, notification_type, recipient, subject, body, status, created_at)
             VALUES
                (:id, :qid, :type, :recipient, :subject, :body, :status, :created_at)',
            tbl_notif()
        );
        $stmt_n = $db->prepare($sql_notif);
        $stmt_n->execute([
            ':id'        => $notif_id,
            ':qid'       => $quote['id'],
            ':type'      => 'email',
            ':recipient' => ADMIN_NOTIFICATION_EMAIL,
            ':subject'   => "New Quote Submission: {$company_name}",
            ':body'      => "Quote submitted by {$contact_name}. Monthly: \${$refreshed['monthly_total']}",
            ':status'    => 'pending',
            ':created_at' => $now,
        ]);

        $db->commit();

    } catch (PDOException $e) {
        $db->rollBack();
        app_log('ERROR', 'Failed to submit quote: ' . $e->getMessage());
        error_response('Failed to submit quote: ' . $e->getMessage(), 500);
    }

    return get_quote_by_token($db, $token);
}

/**
 * Update the most recent notification status for a quote.
 *
 * @param PDO    $db       Database connection.
 * @param string $quote_id Quote UUID.
 * @param string $status   New status (sent, failed).
 * @param string|null $error Error message if failed.
 */
function update_notification_status(PDO $db, string $quote_id, string $status, ?string $error = null): void
{
    $now = utc_now();
    $sql = sprintf(
        'UPDATE %s SET
            status = :status,
            attempts = attempts + 1,
            last_attempt_at = :now,
            sent_at = :sent,
            error_message = :err
         WHERE quote_id = :qid
         ORDER BY created_at DESC
         LIMIT 1',
        tbl_notif()
    );

    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':status' => $status,
        ':now'    => $now,
        ':sent'   => $status === 'sent' ? $now : null,
        ':err'    => $error,
        ':qid'    => $quote_id,
    ]);
}

// --------------------------------------------------------------------------
// Admin functions
// --------------------------------------------------------------------------

/**
 * List quotes with pagination and optional filters.
 *
 * @param PDO         $db     Database connection.
 * @param int         $skip   Offset.
 * @param int         $limit  Max results.
 * @param string|null $status Filter by status.
 * @param string|null $search Search in company_name, contact_name, contact_email.
 * @return array{quotes: array, total: int}
 */
function list_quotes(PDO $db, int $skip = 0, int $limit = 100, ?string $status = null, ?string $search = null): array
{
    $where = [];
    $params = [];

    if ($status !== null && $status !== '') {
        $where[] = 'q.status = :status';
        $params[':status'] = $status;
    }

    if ($search !== null && $search !== '') {
        $where[] = '(q.company_name LIKE :search OR q.contact_name LIKE :search2 OR q.contact_email LIKE :search3)';
        $term = '%' . $search . '%';
        $params[':search']  = $term;
        $params[':search2'] = $term;
        $params[':search3'] = $term;
    }

    $where_sql = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';

    // Get total count
    $count_sql = sprintf('SELECT COUNT(*) FROM %s q %s', tbl_quotes(), $where_sql);
    $stmt = $db->prepare($count_sql);
    $stmt->execute($params);
    $total = (int)$stmt->fetchColumn();

    // Get paginated results with item count
    $list_sql = sprintf(
        'SELECT q.*, (SELECT COUNT(*) FROM %s qi WHERE qi.quote_id = q.id) AS item_count
         FROM %s q
         %s
         ORDER BY q.created_at DESC
         LIMIT :lim OFFSET :off',
        tbl_items(), tbl_quotes(), $where_sql
    );

    $stmt2 = $db->prepare($list_sql);
    foreach ($params as $k => $v) {
        $stmt2->bindValue($k, $v);
    }
    $stmt2->bindValue(':lim', $limit, PDO::PARAM_INT);
    $stmt2->bindValue(':off', $skip, PDO::PARAM_INT);
    $stmt2->execute();
    $rows = $stmt2->fetchAll();

    $quotes = array_map(function ($row) {
        return [
            'id'             => $row['id'],
            'access_token'   => $row['access_token'],
            'status'         => $row['status'],
            'company_name'   => $row['company_name'],
            'contact_name'   => $row['contact_name'],
            'contact_email'  => $row['contact_email'],
            'employee_count' => $row['employee_count'] !== null ? (int)$row['employee_count'] : null,
            'monthly_total'  => number_format((float)$row['monthly_total'], 2, '.', ''),
            'setup_total'    => number_format((float)$row['setup_total'], 2, '.', ''),
            'item_count'     => (int)$row['item_count'],
            'submitted_at'   => format_datetime($row['submitted_at']),
            'created_at'     => format_datetime($row['created_at']),
        ];
    }, $rows);

    return ['quotes' => $quotes, 'total' => $total];
}

/**
 * Get dashboard statistics for quotes.
 *
 * @param PDO $db Database connection.
 * @return array Statistics matching QuoteStatsResponse schema.
 */
function get_stats(PDO $db): array
{
    $tbl = tbl_quotes();

    // Total quotes
    $stmt = $db->query("SELECT COUNT(*) FROM {$tbl}");
    $total_quotes = (int)$stmt->fetchColumn();

    // Quotes by status
    $stmt2 = $db->query("SELECT status, COUNT(*) as cnt FROM {$tbl} GROUP BY status");
    $status_rows = $stmt2->fetchAll();
    $quotes_by_status = [];
    foreach ($status_rows as $r) {
        $quotes_by_status[$r['status']] = (int)$r['cnt'];
    }

    // Total values for submitted quotes
    $placeholders = implode(',', array_fill(0, count(SUBMITTED_STATUSES), '?'));
    $val_sql = "SELECT COALESCE(SUM(monthly_total), 0), COALESCE(SUM(setup_total), 0) FROM {$tbl} WHERE status IN ({$placeholders})";
    $stmt3 = $db->prepare($val_sql);
    $stmt3->execute(SUBMITTED_STATUSES);
    $vals = $stmt3->fetch(PDO::FETCH_NUM);
    $total_monthly_value = (float)$vals[0];
    $total_setup_value   = (float)$vals[1];

    // Quotes this month
    $month_start = gmdate('Y-m-01 00:00:00');
    $stmt4 = $db->prepare("SELECT COUNT(*) FROM {$tbl} WHERE created_at >= ?");
    $stmt4->execute([$month_start]);
    $quotes_this_month = (int)$stmt4->fetchColumn();

    // Submitted this month
    $stmt5 = $db->prepare("SELECT COUNT(*) FROM {$tbl} WHERE submitted_at >= ? AND submitted_at IS NOT NULL");
    $stmt5->execute([$month_start]);
    $quotes_submitted_this_month = (int)$stmt5->fetchColumn();

    // Calculations
    $submitted_count = 0;
    foreach (SUBMITTED_STATUSES as $s) {
        $submitted_count += ($quotes_by_status[$s] ?? 0);
    }

    $average_monthly_value = $submitted_count > 0
        ? round($total_monthly_value / $submitted_count, 2)
        : 0.00;

    $draft_count = $quotes_by_status['draft'] ?? 0;
    $total_started = $draft_count + $submitted_count;
    $conversion_rate = $total_started > 0
        ? round(($submitted_count / $total_started) * 100, 2)
        : 0.00;

    return [
        'total_quotes'                => $total_quotes,
        'quotes_by_status'            => $quotes_by_status,
        'total_monthly_value'         => number_format($total_monthly_value, 2, '.', ''),
        'total_setup_value'           => number_format($total_setup_value, 2, '.', ''),
        'quotes_this_month'           => $quotes_this_month,
        'quotes_submitted_this_month' => $quotes_submitted_this_month,
        'average_monthly_value'       => number_format($average_monthly_value, 2, '.', ''),
        'conversion_rate'             => number_format($conversion_rate, 2, '.', ''),
    ];
}

/**
 * Admin update of a quote's status and/or expiration.
 *
 * @param PDO    $db         Database connection.
 * @param string $quote_id   Quote UUID.
 * @param array  $data       Update data (status, expires_at).
 * @param string $admin_user Admin username for audit trail.
 * @return array Updated quote row.
 */
function admin_update_quote(PDO $db, string $quote_id, array $data, string $admin_user = 'admin'): array
{
    $quote = get_quote_by_id($db, $quote_id);

    $changes = [];
    $sets = [];
    $params = [':id' => $quote_id];

    if (isset($data['status']) && $data['status'] !== $quote['status']) {
        if (!in_array($data['status'], VALID_STATUSES, true)) {
            error_response("Invalid status: {$data['status']}", 400);
        }
        $old_status = $quote['status'];
        $sets[] = 'status = :status';
        $params[':status'] = $data['status'];
        $changes[] = "status: {$old_status} -> {$data['status']}";
    }

    if (isset($data['expires_at'])) {
        $sets[] = 'expires_at = :expires';
        $params[':expires'] = $data['expires_at'];
        $changes[] = "expires_at: {$data['expires_at']}";
    }

    if (!empty($sets)) {
        $sets[] = 'updated_at = :now';
        $params[':now'] = utc_now();

        $sql = sprintf('UPDATE %s SET %s WHERE id = :id', tbl_quotes(), implode(', ', $sets));
        $stmt = $db->prepare($sql);
        $stmt->execute($params);

        log_activity(
            $db, $quote_id, 'admin_update',
            "Admin update by {$admin_user}: " . implode(', ', $changes)
        );
    }

    return get_quote_by_id($db, $quote_id);
}
