#!/usr/bin/env bash
# Adds POST /v1/get_activity_time_slot_supplier_inventory_booking
#   - appends getActivityTimeSlotSupplierInventoryForBooking to
#     controllers/activityFrontController.js
#   - inserts the route into bw_activity_manager_api.js, just above
#     /v1/get_activity_supplier_inventory_booking
# Idempotent. Keeps .bak files. Rolls both files back if the result does not parse.
#
# Run from the project root:  bash apply_time_slot_booking_patch.sh
set -euo pipefail

CTRL="controllers/activityFrontController.js"
API="bw_activity_manager_api.js"
SNIP="$(mktemp)"

test -f "$CTRL" && test -f "$API" || { echo "run me from the project root"; exit 1; }

if grep -q getActivityTimeSlotSupplierInventoryForBooking "$CTRL"; then
  echo "already applied - nothing to do"; exit 0
fi

cp "$CTRL" "$CTRL.bak"
cp "$API"  "$API.bak"

rollback () {
  echo "FAILED - rolling both files back"
  mv "$CTRL.bak" "$CTRL"
  mv "$API.bak"  "$API"
  rm -f "$SNIP"
  exit 1
}
trap rollback ERR

printf '\n\n' >> "$CTRL"
cat >> "$CTRL" <<'CONTROLLER_EOF'
//=========================================
// Get activity TIME SLOT supplier inventory for booking
//=========================================
/**
 * The slot-keyed twin of `getActivitySupplierInventoryForBooking`
 * (`/v1/get_activity_supplier_inventory_booking`), reading
 * `activity_time_slot_supplier_inventory` instead of
 * `activity_supplier_inventory`.
 *
 * WHAT IS THE SAME
 * ---------------------------------------------------------------------------
 * Deliberate parity, so a caller can move between the two without a mapping
 * table: the three LEFT JOINs onto `fdk_holidays`, the buy-side field list, the
 * `priority ASC, adult_purchase_price ASC, scpi.from_day DESC` ordering, the
 * row-grouping into one entry per inventory row with a nested
 * `cancellation_policy.items[]`, and the `replyCode` / `replyMsg` / `data`
 * envelope returned with HTTP 200 for business errors.
 *
 * WHAT IS DIFFERENT, AND WHY
 * ---------------------------------------------------------------------------
 * 1. `category_id` is replaced by `from_time` / `to_time` on every row, and the
 *    request filters are `time_from` / `time_to`. They are EXACT matches
 *    (from_time = ?, to_time = ?), each optional and applied independently -
 *    a booking is for one specific slot, and omitting both returns every slot
 *    on the date, the way omitting category_id returns every category today.
 *
 * 2. ORDER BY GAINS `tsi.from_time ASC` AS ITS LEADING KEY. This is the one
 *    ordering change. With no time filter the result spans several slots, and
 *    the /v1 ordering would interleave 10:00 and 14:00 suppliers by priority -
 *    a caller rendering a slot picker would have to re-sort. Within one slot
 *    the ordering is unchanged: priority, then purchase price. Served by
 *    idx_ts_sup_inv_activity_date_time.
 *
 * 3. `travel_date`, `time_from` and `time_to` ARE VALIDATED. /v1 passes them
 *    straight into the query; they are bound parameters so this is not an
 *    injection, but MySQL casts a malformed date or time to NULL, which matches
 *    no row and is indistinguishable from "no inventory for that slot". That is
 *    recorded as a finding against /v1 in ARCHITECTURE_NOTES §18.15, not a
 *    behaviour to reproduce. `time_from` / `time_to` also accept "10:00" and
 *    normalise it to "10:00:00", matching what create_time_slot stores.
 *
 * 4. NO AUTHENTICATION, matching its /v1 sibling (decision 2026-09-13). This
 *    route ships the BUY side - purchase prices, supplier name, company, email
 *    and mobile, our sourcing priority - and anyone who can reach /v1 can read
 *    it. That is the same exposure §18.15 already records for
 *    /v1/get_activity_supplier_inventory_booking; it is inherited here for
 *    symmetry, not endorsed. Gate both together, or port this to /xApi/* where
 *    a Bearer token is mandatory.
 */

/** "10:00" | "9:30:00" -> "10:00:00". null when unparseable. */
function bkNormalizeTime(value) {
    if (value === null || value === undefined) return null;
    const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(String(value).trim());
    if (!m) return null;
    const h = Number(m[1]);
    const mi = Number(m[2]);
    const s = m[3] === undefined ? 0 : Number(m[3]);
    if (h > 23 || mi > 59 || s > 59) return null;
    return `${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}

/** "2026-10-01" -> "2026-10-01", strictly. null otherwise. */
function bkNormalizeDate(value) {
    if (value === null || value === undefined) return null;
    const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value).trim());
    if (!m) return null;
    const y = Number(m[1]);
    const mo = Number(m[2]);
    const d = Number(m[3]);
    const dt = new Date(y, mo - 1, d);
    // Rejects 2026-02-30 and friends, which Date would roll forward silently.
    if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
    return `${m[1]}-${m[2]}-${m[3]}`;
}

exports.getActivityTimeSlotSupplierInventoryForBooking = async function (req, callback) {

    const {
        activity_id,
        travel_date,
        time_from,
        time_to,
        supplier_id
    } = req.body || {};

    if (!activity_id) {
        return callback(200, null, {
            replyCode: "error",
            replyMsg: "activity_id is required."
        });
    }

    if (!travel_date) {
        return callback(200, null, {
            replyCode: "error",
            replyMsg: "travel_date is required."
        });
    }

    const activityDate = bkNormalizeDate(travel_date);

    if (!activityDate) {
        return callback(200, null, {
            replyCode: "error",
            replyMsg: "Invalid travel_date, use YYYY-MM-DD."
        });
    }

    let fromTime = null;
    let toTime = null;

    if (time_from !== undefined && time_from !== null && time_from !== '') {
        fromTime = bkNormalizeTime(time_from);
        if (!fromTime) {
            return callback(200, null, {
                replyCode: "error",
                replyMsg: "Invalid time_from, use HH:mm or HH:mm:ss."
            });
        }
    }

    if (time_to !== undefined && time_to !== null && time_to !== '') {
        toTime = bkNormalizeTime(time_to);
        if (!toTime) {
            return callback(200, null, {
                replyCode: "error",
                replyMsg: "Invalid time_to, use HH:mm or HH:mm:ss."
            });
        }
    }

    try {
        const connection = await pool.promise().getConnection();
        try {

            let where = `
                tsi.activity_id = ?
                AND tsi.activity_date = ?
                AND tsi.status = 1
            `;

            const params = [
                activity_id,
                activityDate
            ];

            if (supplier_id) {
                where += ` AND tsi.supplier_id = ?`;
                params.push(supplier_id);
            }

            // Independent and exact. A caller that knows only the start of the
            // slot sends time_from alone; one that has the guest's full
            // selection sends both.
            if (fromTime) {
                where += ` AND tsi.from_time = ?`;
                params.push(fromTime);
            }

            if (toTime) {
                where += ` AND tsi.to_time = ?`;
                params.push(toTime);
            }

            const [rows] = await connection.query(
                `
                SELECT
                    tsi.id,
                    tsi.activity_id,
                    tsi.activity_date,
                    tsi.from_time,
                    tsi.to_time,
                    tsi.group_key,
                    tsi.supplier_id,
                    s.supplier_name,
                    s.supplier_company_name,
                    s.supplier_email,
                    s.supplier_mobile,
                    tsi.currency,
                    tsi.adult_purchase_price,
                    tsi.child_purchase_price,
                    tsi.infant_purchase_price,
                    tsi.available_quantity,
                    tsi.total_quantity,
                    tsi.priority,
                    tsi.supplier_cancellation_policy_id,
                    scp.title AS policy_name,
                    scpi.id AS policy_item_id,
                    scpi.from_day,
                    scpi.to_day,
                    scpi.deduction
                FROM activity_time_slot_supplier_inventory tsi
                LEFT JOIN ${Config.DB_PREFIX}fdk_holidays.suppliers s
                    ON s.id = tsi.supplier_id
                LEFT JOIN ${Config.DB_PREFIX}fdk_holidays.supplier_cancellation_policies scp
                    ON scp.id = tsi.supplier_cancellation_policy_id
                LEFT JOIN ${Config.DB_PREFIX}fdk_holidays.supplier_cancellation_policy_items scpi
                    ON scpi.cancellation_policy_id = scp.id
                WHERE ${where}
                ORDER BY
                    tsi.from_time ASC,
                    tsi.to_time ASC,
                    tsi.priority ASC,
                    tsi.adult_purchase_price ASC,
                    scpi.from_day DESC
                `,
                params
            );

            // One entry per inventory row. The policy-items JOIN multiplies
            // rows, so the same tsi.id arrives once per policy item and the
            // map collapses them - identical to the /v1 grouping.
            const suppliers = {};

            rows.forEach(row => {
                if (!suppliers[row.id]) {
                    suppliers[row.id] = {
                        id: row.id,
                        activity_id: row.activity_id,
                        activity_date: row.activity_date,
                        from_time: row.from_time,
                        to_time: row.to_time,
                        group_key: row.group_key,
                        supplier_id: row.supplier_id,
                        supplier_name: row.supplier_name,
                        supplier_company_name: row.supplier_company_name,
                        supplier_email: row.supplier_email,
                        supplier_mobile: row.supplier_mobile,
                        currency: row.currency,
                        adult_purchase_price: row.adult_purchase_price,
                        child_purchase_price: row.child_purchase_price,
                        infant_purchase_price: row.infant_purchase_price,
                        available_quantity: row.available_quantity,
                        total_quantity: row.total_quantity,
                        priority: row.priority,
                        cancellation_policy: {
                            id: row.supplier_cancellation_policy_id,
                            name: row.policy_name,
                            items: []
                        }
                    };
                }

                if (row.policy_item_id) {
                    suppliers[row.id]
                        .cancellation_policy
                        .items
                        .push({
                            id: row.policy_item_id,
                            from_day: row.from_day,
                            to_day: row.to_day,
                            deduction: row.deduction
                        });
                }
            });

            return callback(200, null, {
                replyCode: "success",
                replyMsg: "Time slot supplier inventory fetched successfully.",
                data: Object.values(suppliers)
            });

        } finally {
            try { await connection.release(); } catch { }
        }
    } catch (e) {
        return callback(500, null, {
            replyCode: "error",
            replyMsg: e.sqlMessage || e.message
        });
    }

};
CONTROLLER_EOF

cat > "$SNIP" <<'ROUTE_EOF'
// ---- TIME SLOT SUPPLIER INVENTORY (booking / sourcing read) ----
// The slot-keyed twin of /v1/get_activity_supplier_inventory_booking below.
// Filters by time_from / time_to (exact slot match) instead of category_id.
// Like its sibling, this route has NO authentication - see the note in
// activityFrontController.getActivityTimeSlotSupplierInventoryForBooking.

app.post('/v1/get_activity_time_slot_supplier_inventory_booking', (req, res) => {
    ACTIVITY_FRONT.getActivityTimeSlotSupplierInventoryForBooking(req, (http_status_code, err, response) => {
        if (err) {
            console.error(err);
            return res.status(http_status_code).send(err.message);
        }
        res.status(http_status_code).send(response);
    });
});

ROUTE_EOF

SNIP="$SNIP" python3 - <<'PY'
import io, os, sys
p = "bw_activity_manager_api.js"
s = io.open(p, encoding="utf-8").read()
anchor = "app.post('/v1/get_activity_supplier_inventory_booking', (req, res) => {"
if s.count(anchor) != 1:
    sys.exit("anchor found %d times, expected 1" % s.count(anchor))
block = io.open(os.environ["SNIP"], encoding="utf-8").read()
io.open(p, "w", encoding="utf-8").write(s.replace(anchor, block + anchor, 1))
print("route inserted")
PY

node --check "$CTRL"
node --check "$API"

trap - ERR
rm -f "$SNIP"
echo "SYNTAX OK - patch applied. Backups: $CTRL.bak, $API.bak"
