// CARD Asset Ownership API Routes
// Proxies CARD operations (confirm, decline, redirect, search) and orchestrates
// the two-step update_token flow for mutations.

const express = require('express');
const pool = require('../db');
const { requireAuth, requireGroup, requireTeam } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const {
    isConfigured,
    missingVars,
    getTeams,
    getTeamAssets,
    getOwner,
    confirmAsset,
    declineAsset,
    redirectAsset,
    resolveAssetId,
    searchByIvantiHostId,
    searchByAssetId,
} = require('../helpers/cardApi');

// ---------------------------------------------------------------------------
// Error classification — maps CARD API / token errors to client responses
// ---------------------------------------------------------------------------
function handleCardError(err, res) {
    const msg = err.message || String(err);
    console.error('[card-api]', msg);

    if (msg.includes('Token acquisition failed')) {
        if (msg.includes('HTTP 401')) {
            return res.status(401).json({ error: 'CARD authorization failed. Check service account credentials.' });
        }
        if (msg.includes('HTTP 403')) {
            return res.status(403).json({ error: 'CARD access denied. The service account may not be onboarded with the CARD team.' });
        }
        if (msg.includes('HTTP 525')) {
            return res.status(502).json({ error: 'CARD LDAP error. The service account may not be provisioned correctly.' });
        }
    }

    if (msg.includes('401')) {
        return res.status(401).json({ error: 'CARD token expired or invalid. The request has been retried once automatically.' });
    }
    if (msg.includes('403')) {
        return res.status(403).json({ error: 'Insufficient CARD permissions for this operation.' });
    }

    return res.status(502).json({ error: 'CARD API request failed.', details: msg });
}

// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
function createCardApiRouter() {
    const router = express.Router();

    /**
     * GET /status
     *
     * Returns whether the CARD API integration is configured.
     *
     * @response 200 - { configured: true }
     * @response 503 - { configured: false, error: string, missingVars: string[] }
     */
    router.get('/status', requireAuth(), (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ configured: false, error: 'CARD API is not configured.', missingVars });
        }
        res.json({ configured: true });
    });

    /**
     * GET /teams
     *
     * Returns the list of teams from the CARD API.
     *
     * @response 200 - Array of team objects from CARD
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.get('/teams', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        try {
            const result = await getTeams();
            if (result.ok) {
                let body;
                try { body = JSON.parse(result.body); } catch (_) { body = result.body; }
                const teams = Array.isArray(body) ? body : (body && body.teams) || [];
                return res.json(teams);
            }
            let errorBody;
            try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
            return res.status(result.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * GET /teams/:teamName/assets
     *
     * Returns paginated assets for a team filtered by disposition.
     *
     * @param {string} teamName - Team name (path parameter)
     * @query {string} disposition - Required. Asset disposition filter.
     * @query {number} [page] - Page number for pagination.
     * @query {number} [page_size=50] - Number of results per page.
     * @response 200 - { assets: object[], total: number, ... } from CARD
     * @response 400 - { error: string } — missing disposition
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.get('/teams/:teamName/assets', requireAuth(), requireGroup('Admin', 'Standard_User'), requireTeam(), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { teamName } = req.params;
        const { disposition, page, page_size } = req.query;

        // Validate requested team is in user's scope
        if (req.teamScope && !req.teamScope.short.includes(teamName)) {
            return res.status(403).json({
                error: 'Access denied. You do not have access to the requested team.',
                code: 'TEAM_ACCESS_DENIED',
                requested: teamName,
                allowed: req.teamScope.short
            });
        }

        if (!disposition) {
            return res.status(400).json({ error: 'disposition query parameter is required.' });
        }

        try {
            const result = await getTeamAssets(teamName, {
                disposition,
                page: page ? parseInt(page, 10) : undefined,
                pageSize: page_size ? parseInt(page_size, 10) : 50,
            });

            if (result.ok) {
                let body;
                try { body = JSON.parse(result.body); } catch (_) { body = result.body; }

                let resultCount = 0;
                if (body && typeof body === 'object' && typeof body.total === 'number') {
                    resultCount = body.total;
                } else if (body && Array.isArray(body.assets)) {
                    resultCount = body.assets.length;
                }
                logAudit({
                    userId: req.user.id,
                    username: req.user.username,
                    action: 'card_search',
                    entityType: 'card_asset',
                    entityId: teamName,
                    details: { disposition, resultCount },
                    ipAddress: req.ip,
                });

                return res.json(body);
            }

            let errorBody;
            try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
            return res.status(result.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * GET /owner/:assetId
     *
     * Returns the CARD owner record for a given asset ID.
     *
     * @param {string} assetId - CARD asset identifier (path parameter)
     * @response 200 - CARD owner/asset object
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.get('/owner/:assetId', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { assetId } = req.params;

        try {
            const result = await getOwner(assetId);
            if (result.ok) {
                let body;
                try { body = JSON.parse(result.body); } catch (_) { body = result.body; }
                return res.json(body);
            }
            let errorBody;
            try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
            return res.status(result.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * POST /queue/:queueItemId/confirm
     *
     * Confirms ownership of a CARD asset for a queue item. Fetches the owner
     * record to obtain the update_token, then calls the CARD confirm endpoint.
     * Marks the queue item as complete on success.
     *
     * @param {string} queueItemId - Queue item ID (path parameter)
     * @body {string} teamName - Team name to confirm ownership for (required)
     * @body {string} assetId - CARD asset identifier (required)
     * @body {string} [comment] - Optional comment for the confirmation
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields or item not pending
     * @response 404 - { error: string } — queue item not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/queue/:queueItemId/confirm', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { queueItemId } = req.params;
        const { teamName, assetId: rawAssetId, comment } = req.body;

        if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
            return res.status(400).json({ error: 'teamName is required.' });
        }
        if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
            return res.status(400).json({ error: 'assetId is required.' });
        }

        // Resolve bare IP to full CARD asset ID — try Ivanti host_id fast path first
        let assetId = rawAssetId.trim();
        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            let resolved = null;

            // Fast path: look up the finding's host_id and search CARD directly
            const findingRow = await pool.query(
                'SELECT host_id FROM ivanti_findings WHERE ip_address = $1 LIMIT 1',
                [assetId]
            ).then(r => r.rows[0]).catch(() => null);

            if (findingRow && findingRow.host_id) {
                try {
                    const searchResult = await searchByIvantiHostId(findingRow.host_id);
                    if (searchResult.ok) {
                        const searchData = JSON.parse(searchResult.body);
                        resolved = searchData._id || searchData.asset_id || searchData.id || null;
                    }
                } catch (_) { /* fall through */ }
            }

            // Fallback: suffix guessing
            if (!resolved) {
                resolved = await resolveAssetId(assetId);
            }
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const { rows } = await pool.query(
                'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
                [queueItemId, req.user.id, 'CARD']
            );
            const item = rows[0];

            if (!item) {
                return res.status(404).json({ error: 'Queue item not found.' });
            }
            if (item.status !== 'pending') {
                return res.status(400).json({ error: 'Only pending queue items can be executed.' });
            }

            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
                let errorBody;
                try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
                return res.status(ownerResult.status).json(errorBody);
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                const errMsg = 'update_token not found in owner record.';
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: null }, ipAddress: req.ip });
                return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
            }

            const confirmResult = await confirmAsset(assetId, teamName.trim(), updateToken, comment || '');

            if (confirmResult.ok) {
                await pool.query(
                    "UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
                    [queueItemId]
                );

                let cardResponse;
                try { cardResponse = JSON.parse(confirmResult.body); } catch (_) { cardResponse = confirmResult.body; }

                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_confirm', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: confirmResult.status }, ipAddress: req.ip });

                return res.json({ success: true, cardResponse });
            }

            const errMsg = `Confirm failed: HTTP ${confirmResult.status}`;
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: confirmResult.status }, ipAddress: req.ip });
            let errorBody;
            try { errorBody = JSON.parse(confirmResult.body); } catch (_) { errorBody = { error: confirmResult.body }; }
            return res.status(confirmResult.status).json(errorBody);
        } catch (err) {
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
            return handleCardError(err, res);
        }
    });

    /**
     * POST /queue/:queueItemId/decline
     *
     * Declines ownership of a CARD asset for a queue item. Fetches the owner
     * record to obtain the update_token, then calls the CARD decline endpoint.
     * Marks the queue item as complete on success.
     *
     * @param {string} queueItemId - Queue item ID (path parameter)
     * @body {string} teamName - Team name declining ownership (required)
     * @body {string} assetId - CARD asset identifier (required)
     * @body {string} [comment] - Optional comment for the decline
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields or item not pending
     * @response 404 - { error: string } — queue item not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/queue/:queueItemId/decline', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { queueItemId } = req.params;
        const { teamName, assetId: rawAssetId, comment } = req.body;

        if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
            return res.status(400).json({ error: 'teamName is required.' });
        }
        if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
            return res.status(400).json({ error: 'assetId is required.' });
        }

        // Resolve bare IP to full CARD asset ID — try Ivanti host_id fast path first
        let assetId = rawAssetId.trim();
        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            let resolved = null;

            // Fast path: look up the finding's host_id and search CARD directly
            const findingRow = await pool.query(
                'SELECT host_id FROM ivanti_findings WHERE ip_address = $1 LIMIT 1',
                [assetId]
            ).then(r => r.rows[0]).catch(() => null);

            if (findingRow && findingRow.host_id) {
                try {
                    const searchResult = await searchByIvantiHostId(findingRow.host_id);
                    if (searchResult.ok) {
                        const searchData = JSON.parse(searchResult.body);
                        resolved = searchData._id || searchData.asset_id || searchData.id || null;
                    }
                } catch (_) { /* fall through */ }
            }

            // Fallback: suffix guessing
            if (!resolved) {
                resolved = await resolveAssetId(assetId);
            }
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const { rows } = await pool.query(
                'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
                [queueItemId, req.user.id, 'CARD']
            );
            const item = rows[0];

            if (!item) {
                return res.status(404).json({ error: 'Queue item not found.' });
            }
            if (item.status !== 'pending') {
                return res.status(400).json({ error: 'Only pending queue items can be executed.' });
            }

            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
                let errorBody;
                try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
                return res.status(ownerResult.status).json(errorBody);
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                const errMsg = 'update_token not found in owner record. The asset may have already been actioned or the owner record is in an unexpected state.';
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: null, ownerResponse: ownerData }, ipAddress: req.ip });
                return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
            }

            const declineResult = await declineAsset(assetId, teamName.trim(), updateToken, comment || '');

            if (declineResult.ok) {
                await pool.query(
                    "UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
                    [queueItemId]
                );

                let cardResponse;
                try { cardResponse = JSON.parse(declineResult.body); } catch (_) { cardResponse = declineResult.body; }

                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_decline', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: declineResult.status }, ipAddress: req.ip });

                return res.json({ success: true, cardResponse });
            }

            const errMsg = `Decline failed: HTTP ${declineResult.status}`;
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: declineResult.status }, ipAddress: req.ip });
            let errorBody;
            try { errorBody = JSON.parse(declineResult.body); } catch (_) { errorBody = { error: declineResult.body }; }
            return res.status(declineResult.status).json(errorBody);
        } catch (err) {
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
            return handleCardError(err, res);
        }
    });

    /**
     * POST /queue/:queueItemId/redirect
     *
     * Redirects a CARD asset from one team to another for a queue item. Fetches
     * the owner record to obtain the update_token, then calls the CARD redirect
     * endpoint. Marks the queue item as complete on success.
     *
     * @param {string} queueItemId - Queue item ID (path parameter)
     * @body {string} fromTeam - Current owning team (required)
     * @body {string} toTeam - Target team to redirect to (required)
     * @body {string} assetId - CARD asset identifier (required)
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields or item not pending
     * @response 404 - { error: string } — queue item not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/queue/:queueItemId/redirect', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { queueItemId } = req.params;
        const { fromTeam, toTeam, assetId: rawAssetId } = req.body;

        if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) {
            return res.status(400).json({ error: 'fromTeam is required.' });
        }
        if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) {
            return res.status(400).json({ error: 'toTeam is required.' });
        }
        if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
            return res.status(400).json({ error: 'assetId is required.' });
        }

        // Resolve bare IP to full CARD asset ID — try Ivanti host_id fast path first
        let assetId = rawAssetId.trim();
        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            let resolved = null;

            // Fast path: look up the finding's host_id and search CARD directly
            const findingRow = await pool.query(
                'SELECT host_id FROM ivanti_findings WHERE ip_address = $1 LIMIT 1',
                [assetId]
            ).then(r => r.rows[0]).catch(() => null);

            if (findingRow && findingRow.host_id) {
                try {
                    const searchResult = await searchByIvantiHostId(findingRow.host_id);
                    if (searchResult.ok) {
                        const searchData = JSON.parse(searchResult.body);
                        resolved = searchData._id || searchData.asset_id || searchData.id || null;
                    }
                } catch (_) { /* fall through */ }
            }

            // Fallback: suffix guessing
            if (!resolved) {
                resolved = await resolveAssetId(assetId);
            }
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const { rows } = await pool.query(
                'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
                [queueItemId, req.user.id, 'CARD']
            );
            const item = rows[0];

            if (!item) {
                return res.status(404).json({ error: 'Queue item not found.' });
            }
            if (item.status !== 'pending') {
                return res.status(400).json({ error: 'Only pending queue items can be executed.' });
            }

            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
                let errorBody;
                try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
                return res.status(ownerResult.status).json(errorBody);
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                const errMsg = 'update_token not found in owner record.';
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: null }, ipAddress: req.ip });
                return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
            }

            const redirectResult = await redirectAsset(assetId, fromTeam.trim(), toTeam.trim(), updateToken);

            if (redirectResult.ok) {
                await pool.query(
                    "UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
                    [queueItemId]
                );

                let cardResponse;
                try { cardResponse = JSON.parse(redirectResult.body); } catch (_) { cardResponse = redirectResult.body; }

                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_redirect', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, fromTeam: fromTeam.trim(), toTeam: toTeam.trim(), cardStatus: redirectResult.status }, ipAddress: req.ip });

                return res.json({ success: true, cardResponse });
            }

            const errMsg = `Redirect failed: HTTP ${redirectResult.status}`;
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: redirectResult.status }, ipAddress: req.ip });
            let errorBody;
            try { errorBody = JSON.parse(redirectResult.body); } catch (_) { errorBody = { error: redirectResult.body }; }
            return res.status(redirectResult.status).json(errorBody);
        } catch (err) {
            logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
            return handleCardError(err, res);
        }
    });

    /**
     * GET /owner-lookup/:ip
     *
     * Resolve an IP to a CARD asset ID and return the full owner record.
     * Used by the CARD Action Modal to display ownership state before
     * confirm/decline/redirect operations.
     *
     * @param {string} ip - IP address (path parameter)
     * @query {string} [quick] - Set to "1" for quick mode (CTEC only, 15s timeout). Used for tooltip lookups.
     * @query {string} [hostId] - Ivanti Host ID (integer). When provided, uses CARD asset-search for faster resolution.
     * @response 200 - { asset_id, ip, confirmed, unconfirmed, declined, candidate, update_token }
     * @response 400 - { error: string } — missing IP
     * @response 404 - { error: string } — IP not found in CARD
     * @response 504 - { error: string, timeout: true } — CARD lookup timed out
     * @response 503 - { error: string } — CARD not configured
     */
    router.get('/owner-lookup/:ip', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const ip = req.params.ip;
        if (!ip || !ip.trim()) {
            return res.status(400).json({ error: 'IP address is required.' });
        }

        // Use quick mode (CTEC only, 15s timeout) for tooltip lookups
        const quick = req.query.quick === '1';
        const hostId = req.query.hostId;

        // Fast path: if Ivanti hostId is provided, try asset-search first
        // The asset-search returns the full record including owner data, so we can
        // often return directly without a separate getOwner() call.
        // If update_token is missing, do a follow-up getOwner() to fetch it.
        let assetId = null;
        if (hostId && /^\d+$/.test(hostId)) {
            try {
                const searchResult = await searchByIvantiHostId(hostId);
                if (searchResult.ok) {
                    const searchData = JSON.parse(searchResult.body);
                    const assets = searchData.assets || [];
                    if (assets.length > 0) {
                        const asset = assets[0];
                        const owner = asset.owner || {};
                        let updateToken = owner.update_token || null;

                        // If no update_token, fetch it from the owner endpoint using the resolved _id
                        if (!updateToken && asset._id) {
                            try {
                                const ownerResult = await getOwner(asset._id);
                                if (ownerResult.ok) {
                                    const ownerData = JSON.parse(ownerResult.body);
                                    updateToken = (ownerData.owner && ownerData.owner.update_token) || null;
                                }
                            } catch (_) { /* best effort */ }
                        }

                        return res.json({
                            asset_id: asset._id || null,
                            ip: ip.trim(),
                            confirmed: owner.confirmed || null,
                            unconfirmed: owner.unconfirmed || null,
                            declined: owner.declined || [],
                            candidate: owner.candidate || [],
                            update_token: updateToken,
                        });
                    }
                }
            } catch (_) {
                // Fall through to suffix resolution
            }
        }

        // Fallback: resolve via IP + suffix guessing
        if (!assetId) {
            try {
                assetId = await resolveAssetId(ip.trim(), quick ? { quick: true } : undefined);
            } catch (err) {
                if (err.message === 'CARD_TIMEOUT') {
                    return res.status(504).json({ error: 'CARD lookup timed out', timeout: true });
                }
                return handleCardError(err, res);
            }
        }

        if (!assetId) {
            return res.status(404).json({ error: `Asset not found in CARD for IP: ${ip}` });
        }

        // Fetch full owner record
        try {
            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                return res.status(ownerResult.status).json({ error: `Failed to fetch owner: HTTP ${ownerResult.status}` });
            }

            const data = JSON.parse(ownerResult.body);
            const owner = data.owner || {};

            res.json({
                asset_id: assetId,
                ip: ip.trim(),
                confirmed: owner.confirmed || null,
                unconfirmed: owner.unconfirmed || null,
                declined: owner.declined || [],
                candidate: owner.candidate || [],
                update_token: owner.update_token || null,
            });
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * POST /owner/:assetId/confirm
     *
     * Directly confirm ownership of a CARD asset (no queue item required).
     * Fetches the owner record for the update_token, then calls CARD confirm.
     *
     * @param {string} assetId - CARD asset identifier or IP address (path parameter)
     * @body {string} teamName - Team to confirm ownership for (required)
     * @body {string} [comment] - Optional comment
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields
     * @response 404 - { error: string } — asset not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/owner/:assetId/confirm', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        let assetId = req.params.assetId;
        const { teamName, comment } = req.body || {};

        if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
            return res.status(400).json({ error: 'teamName is required.' });
        }

        // Resolve bare IP to full CARD asset ID
        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            const resolved = await resolveAssetId(assetId);
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                return res.status(ownerResult.status).json({ error: `Failed to fetch owner record: HTTP ${ownerResult.status}` });
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                return res.status(502).json({ error: 'update_token not found in owner record.' });
            }

            const confirmResult = await confirmAsset(assetId, teamName.trim(), updateToken, comment || '');

            if (confirmResult.ok) {
                let cardResponse;
                try { cardResponse = JSON.parse(confirmResult.body); } catch (_) { cardResponse = confirmResult.body; }
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_confirm_direct', entityType: 'card_asset', entityId: assetId, details: { teamName: teamName.trim(), comment: comment || '' }, ipAddress: req.ip });
                return res.json({ success: true, cardResponse });
            }

            let errorBody;
            try { errorBody = JSON.parse(confirmResult.body); } catch (_) { errorBody = { error: confirmResult.body }; }
            return res.status(confirmResult.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * POST /owner/:assetId/decline
     *
     * Directly decline ownership of a CARD asset (no queue item required).
     * Fetches the owner record for the update_token, then calls CARD decline.
     *
     * @param {string} assetId - CARD asset identifier or IP address (path parameter)
     * @body {string} teamName - Team to decline ownership for (required)
     * @body {string} [comment] - Optional comment
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields
     * @response 404 - { error: string } — asset not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/owner/:assetId/decline', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        let assetId = req.params.assetId;
        const { teamName, comment } = req.body || {};

        if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
            return res.status(400).json({ error: 'teamName is required.' });
        }

        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            const resolved = await resolveAssetId(assetId);
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                return res.status(ownerResult.status).json({ error: `Failed to fetch owner record: HTTP ${ownerResult.status}` });
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                return res.status(502).json({ error: 'update_token not found in owner record.' });
            }

            const declineResult = await declineAsset(assetId, teamName.trim(), updateToken, comment || '');

            if (declineResult.ok) {
                let cardResponse;
                try { cardResponse = JSON.parse(declineResult.body); } catch (_) { cardResponse = declineResult.body; }
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_decline_direct', entityType: 'card_asset', entityId: assetId, details: { teamName: teamName.trim(), comment: comment || '' }, ipAddress: req.ip });
                return res.json({ success: true, cardResponse });
            }

            let errorBody;
            try { errorBody = JSON.parse(declineResult.body); } catch (_) { errorBody = { error: declineResult.body }; }
            return res.status(declineResult.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * POST /owner/:assetId/redirect
     *
     * Directly redirect a CARD asset between teams (no queue item required).
     * Fetches the owner record for the update_token, then calls CARD redirect.
     *
     * @param {string} assetId - CARD asset identifier or IP address (path parameter)
     * @body {string} fromTeam - Current owning team (required)
     * @body {string} toTeam - Target team (required)
     * @response 200 - { success: true, cardResponse: object }
     * @response 400 - { error: string } — missing fields
     * @response 404 - { error: string } — asset not found
     * @response 502 - { error: string } — CARD API failure
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/owner/:assetId/redirect', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        let assetId = req.params.assetId;
        const { fromTeam, toTeam } = req.body || {};

        if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) {
            return res.status(400).json({ error: 'fromTeam is required.' });
        }
        if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) {
            return res.status(400).json({ error: 'toTeam is required.' });
        }

        if (!/\d+-[A-Z]+$/i.test(assetId)) {
            const resolved = await resolveAssetId(assetId);
            if (!resolved) {
                return res.status(404).json({ error: `Asset not found in CARD for: ${assetId}` });
            }
            assetId = resolved;
        }

        try {
            const ownerResult = await getOwner(assetId);
            if (!ownerResult.ok) {
                return res.status(ownerResult.status).json({ error: `Failed to fetch owner record: HTTP ${ownerResult.status}` });
            }

            let ownerData;
            try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
            const updateToken = ownerData.owner && ownerData.owner.update_token;

            if (!updateToken) {
                return res.status(502).json({ error: 'update_token not found in owner record.' });
            }

            const redirectResult = await redirectAsset(assetId, fromTeam.trim(), toTeam.trim(), updateToken);

            if (redirectResult.ok) {
                let cardResponse;
                try { cardResponse = JSON.parse(redirectResult.body); } catch (_) { cardResponse = redirectResult.body; }
                logAudit({ userId: req.user.id, username: req.user.username, action: 'card_redirect_direct', entityType: 'card_asset', entityId: assetId, details: { fromTeam: fromTeam.trim(), toTeam: toTeam.trim() }, ipAddress: req.ip });
                return res.json({ success: true, cardResponse });
            }

            let errorBody;
            try { errorBody = JSON.parse(redirectResult.body); } catch (_) { errorBody = { error: redirectResult.body }; }
            return res.status(redirectResult.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * GET /asset-search/:hostId
     *
     * Search CARD by Ivanti Asset ID (integer). Uses CARD's v2 asset-search
     * endpoint with deep_search to find the associated CARD asset directly,
     * bypassing the IP + suffix guessing flow.
     *
     * @param {string} hostId - Ivanti Host ID (8-digit integer, from ivanti_findings.host_id)
     * @response 200 - CARD asset record
     * @response 400 - { error: string } — invalid host ID
     * @response 404 - { error: string } — asset not found
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.get('/asset-search/:hostId', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { hostId } = req.params;
        if (!hostId || !/^\d+$/.test(hostId.trim())) {
            return res.status(400).json({ error: 'hostId must be a numeric Ivanti Asset ID.' });
        }

        try {
            const result = await searchByIvantiHostId(hostId.trim());

            if (result.ok) {
                let body;
                try { body = JSON.parse(result.body); } catch (_) { body = result.body; }

                logAudit({
                    userId: req.user.id,
                    username: req.user.username,
                    action: 'card_asset_search',
                    entityType: 'card_asset',
                    entityId: hostId.trim(),
                    details: { search_type: 'ivanti_host_id' },
                    ipAddress: req.ip,
                });

                return res.json(body);
            }

            if (result.status === 404) {
                return res.status(404).json({ error: `No CARD asset found for Ivanti Host ID: ${hostId}` });
            }

            let errorBody;
            try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
            return res.status(result.status).json(errorBody);
        } catch (err) {
            return handleCardError(err, res);
        }
    });

    /**
     * POST /enrich-batch
     *
     * Batch lookup IPs and/or Ivanti host IDs in CARD to extract Granite loader
     * fields. Accepts an array of IPs, an array of host_ids, or both. For IPs,
     * fetches team assets (paginated, across confirmed, unconfirmed, and
     * candidate dispositions) and matches against the provided IPs. For host_ids,
     * performs direct CARD asset-search lookups. When no team is specified,
     * searches both NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG.
     * Returns enrichment results for each IP.
     *
     * @body {string[]} [ips] - Array of IP address strings (max 200). At least one of ips or host_ids is required.
     * @body {string[]} [host_ids] - Array of Ivanti Host ID strings (max 200). At least one of ips or host_ids is required.
     * @body {string} [team] - Team name to search assets under. Defaults to both NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG if omitted.
     * @response 200 - { results: object[], enriched_count: number, not_found_count: number, total: number }
     *   Each result: { ip: string, found: boolean, equip_inst_id: string|null, hostname: string|null, site_name?: string|null, mgmt_ip_asn?: string|null, responsible_team?: string|null, equipment_class?: string, equip_template?: string|null, equip_status?: string|null, serial_number?: string|null, error?: string }
     * @response 400 - { error: string } — neither ips nor host_ids provided, or exceeds 200 items
     * @response 503 - { error: string, missingVars: string[] } — CARD not configured
     */
    router.post('/enrich-batch', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
        if (!isConfigured) {
            return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
        }

        const { ips, host_ids, team } = req.body || {};

        // Accept either ips array, host_ids array, or both
        const hasIps = Array.isArray(ips) && ips.length > 0;
        const hasHostIds = Array.isArray(host_ids) && host_ids.length > 0;

        if (!hasIps && !hasHostIds) {
            return res.status(400).json({ error: 'ips or host_ids array is required.' });
        }
        if ((ips && ips.length > 200) || (host_ids && host_ids.length > 200)) {
            return res.status(400).json({ error: 'Maximum 200 items per request.' });
        }

        // Build a set of IPs we're looking for
        const targetIps = new Set((ips || []).map(ip => (ip || '').trim()).filter(Boolean));
        const resultMap = {};

        // Direct host_id lookups — for items that have no IP but have a host_id
        if (hasHostIds) {
            for (const hostId of host_ids) {
                if (!hostId) continue;
                const key = `hostId:${hostId}`;
                try {
                    const searchResult = await searchByIvantiHostId(hostId);
                    if (searchResult.ok) {
                        const searchData = JSON.parse(searchResult.body);
                        const assets = searchData.assets || [];
                        if (assets.length > 0) {
                            const asset = assets[0];
                            const assetIp = (asset._id || '').replace(/-[A-Z]+$/i, '');
                            resultMap[key] = { ...extractGraniteFields(asset, assetIp), ip: assetIp };
                        }
                    }
                } catch (_) { /* skip */ }
            }
        }

        // Fast path: look up host_ids from ivanti_findings and use asset-search
        // The asset-search endpoint returns the full enriched record (same as team assets).
        const ipsArray = [...targetIps];
        if (ipsArray.length > 0) {
            try {
                const { rows: findingRows } = await pool.query(
                    `SELECT DISTINCT ON (ip_address) ip_address, host_id
                     FROM ivanti_findings
                     WHERE ip_address = ANY($1) AND host_id IS NOT NULL`,
                    [ipsArray]
                );

                for (const row of findingRows) {
                    if (resultMap[row.ip_address]) continue;
                    try {
                        const searchResult = await searchByIvantiHostId(row.host_id);
                        if (searchResult.ok) {
                            const searchData = JSON.parse(searchResult.body);
                            const assets = searchData.assets || [];
                            if (assets.length > 0) {
                                resultMap[row.ip_address] = extractGraniteFields(assets[0], row.ip_address);
                            }
                        }
                    } catch (_) { /* fall through to paginated lookup */ }
                }
            } catch (err) {
                console.error('[card-api] enrich-batch: Fast path DB lookup failed:', err.message);
            }
        }

        // Direct resolve path: for IPs not found via ivanti_findings, resolve
        // the asset ID via suffix guessing (CTEC first) and fetch the full asset
        // record via asset-search. This returns ncim_discovery, netops_granite, etc.
        const unresolvedIps = ipsArray.filter(ip => !resultMap[ip]);
        if (unresolvedIps.length > 0) {
            const CONCURRENCY = 5;
            for (let i = 0; i < unresolvedIps.length; i += CONCURRENCY) {
                const batch = unresolvedIps.slice(i, i + CONCURRENCY);
                await Promise.all(batch.map(async (ip) => {
                    if (resultMap[ip]) return;
                    try {
                        const assetId = await resolveAssetId(ip, { quick: true });
                        if (assetId) {
                            // Use asset-search to get the full enriched record (30s timeout)
                            const searchResult = await searchByAssetId(assetId, { timeout: 30000 });
                            if (searchResult.ok) {
                                const searchData = JSON.parse(searchResult.body);
                                const assets = searchData.assets || [];
                                if (assets.length > 0) {
                                    resultMap[ip] = extractGraniteFields(assets[0], ip);
                                } else {
                                    // Fallback: asset-search returned empty, try owner record
                                    const ownerResult = await getOwner(assetId);
                                    if (ownerResult.ok) {
                                        const ownerData = JSON.parse(ownerResult.body);
                                        resultMap[ip] = extractGraniteFields(ownerData, ip);
                                    }
                                }
                            } else {
                                // asset-search failed, fall back to owner endpoint
                                const ownerResult = await getOwner(assetId);
                                if (ownerResult.ok) {
                                    const ownerData = JSON.parse(ownerResult.body);
                                    resultMap[ip] = extractGraniteFields(ownerData, ip);
                                }
                            }
                        }
                    } catch (err) {
                        const isTimeout = err.message && (err.message.includes('CARD_TIMEOUT') || err.message.includes('timed out'));
                        if (isTimeout) {
                            console.warn(`[card-api] enrich-batch: Direct resolve for ${ip} timed out`);
                            resultMap[ip] = { _timeout: true };
                        } else {
                            console.warn(`[card-api] enrich-batch: Direct resolve for ${ip} failed:`, err.message);
                        }
                    }
                }));
            }
        }

        let foundCount = Object.keys(resultMap).filter(k => !resultMap[k]._timeout).length;

        // Fallback: paginated team-assets loop for any IPs not resolved by fast path
        // Skip if all unresolved IPs already timed out (heavier calls will also timeout)
        const teams = team ? [team] : ['NTS-AEO-STEAM', 'NTS-AEO-ACCESS-ENG', 'NTS-AEO-ACCESS-OPS'];
        const dispositions = ['confirmed', 'unconfirmed', 'candidate'];
        const stillUnresolved = [...targetIps].filter(ip => !resultMap[ip]);

        for (const teamName of teams) {
            if (stillUnresolved.length === 0 || foundCount >= targetIps.size) break;

            for (const disposition of dispositions) {
                if (foundCount >= targetIps.size) break;

            let page = 1;
            const pageSize = 200;
            let hasMore = true;

            while (hasMore && foundCount < targetIps.size) {
                try {
                    const result = await getTeamAssets(teamName, { disposition, page, pageSize });
                    if (!result.ok) break;

                    const data = JSON.parse(result.body);
                    const assets = data.assets || data.results || (Array.isArray(data) ? data : []);

                    if (assets.length === 0) {
                        hasMore = false;
                        break;
                    }

                    // Match assets against target IPs
                    for (const asset of assets) {
                        const assetId = asset._id || '';
                        // Extract IP from asset ID (e.g., "10.240.78.110-CTEC" → "10.240.78.110")
                        const assetIp = assetId.replace(/-[A-Z]+$/i, '');

                        if (targetIps.has(assetIp) && !resultMap[assetIp]) {
                            resultMap[assetIp] = extractGraniteFields(asset, assetIp);
                            foundCount++;
                        }
                    }

                    // Check if there are more pages
                    const total = data.total || data.count || 0;
                    if (page * pageSize >= total || assets.length < pageSize) {
                        hasMore = false;
                    } else {
                        page++;
                    }
                } catch (err) {
                    console.error(`[card-api] enrich-batch: Error fetching ${teamName}/${disposition} page ${page}:`, err.message);
                    hasMore = false;
                }
            }
        }
        }

        // Build results array in the same order as input IPs
        const results = [];
        let enrichedCount = 0;
        let notFoundCount = 0;

        for (const ip of (ips || [])) {
            const trimmedIp = (ip || '').trim();
            if (!trimmedIp) {
                results.push({ ip: '', found: false, equip_inst_id: null, hostname: null, error: 'Invalid IP' });
                notFoundCount++;
                continue;
            }

            if (resultMap[trimmedIp]) {
                if (resultMap[trimmedIp]._timeout) {
                    results.push({ ip: trimmedIp, found: false, equip_inst_id: null, hostname: null, error: 'CARD lookup timed out — try again' });
                    notFoundCount++;
                } else {
                    results.push({ ip: trimmedIp, found: true, ...resultMap[trimmedIp] });
                    enrichedCount++;
                }
            } else {
                results.push({ ip: trimmedIp, found: false, equip_inst_id: null, hostname: null, error: 'IP not found in CARD' });
                notFoundCount++;
            }
        }

        // Include results for host_id lookups (items without IPs)
        const hostIdResults = [];
        if (hasHostIds) {
            for (const hostId of host_ids) {
                if (!hostId) continue;
                const key = `hostId:${hostId}`;
                if (resultMap[key]) {
                    hostIdResults.push({ host_id: hostId, found: true, ...resultMap[key] });
                    enrichedCount++;
                } else {
                    hostIdResults.push({ host_id: hostId, found: false, equip_inst_id: null, hostname: null, error: 'Host ID not found in CARD' });
                    notFoundCount++;
                }
            }
        }

        res.json({ results, host_id_results: hostIdResults, enriched_count: enrichedCount, not_found_count: notFoundCount, total: (ips || []).length + (host_ids || []).length });
    });

    return router;
}

/**
 * Extract Granite-relevant fields from a CARD asset/owner record.
 * The owner endpoint returns: owner (confirmed/unconfirmed/etc), card_flags, tmp.
 * ncim_discovery is only available from the team assets endpoint.
 */
function extractGraniteFields(asset, ip) {
    // card_flags can be an array or nested in the response differently
    let flags = {};
    if (asset.card_flags) {
        flags = Array.isArray(asset.card_flags) ? (asset.card_flags[0] || {}) : asset.card_flags;
    }
    // Also check if flags are at the top level (owner endpoint format)
    if (!flags.CARD_HOSTNAME && asset.CARD_HOSTNAME) {
        flags = asset;
    }

    const ncim = asset.ncim_discovery || [];
    const granite = asset.netops_granite_allips || [];
    const iseGranite = asset.ise_granite_equipment || [];
    const qualys = asset.qualys_hosts || [];
    const ivanti = asset.ivanti_assets || [];

    // EQUIP_INST_ID — from ncim_discovery or granite (only available from team assets endpoint)
    let equip_inst_id = null;
    let site_name = null;
    let responsible_team = null;
    let hostname = null;

    if (ncim.length > 0) {
        equip_inst_id = ncim[0].EQUIP_INST_ID || null;
        responsible_team = ncim[0].GRANITE_RESP_TEAM || ncim[0].RESPONSIBLE_TEAM || null;
        site_name = ncim[0].SITE_NAME || ncim[0].SITENAME || null;
        hostname = ncim[0].HOSTNAME || null;
    }

    if (!equip_inst_id && Array.isArray(granite) && granite.length > 0) {
        equip_inst_id = granite[0].EQUIP_INST_ID || null;
        if (!site_name) site_name = granite[0].SITE_NAME || null;
        if (!responsible_team) responsible_team = granite[0].RESPONSIBLE_TEAM || null;
    }

    if (!equip_inst_id && Array.isArray(iseGranite) && iseGranite.length > 0) {
        equip_inst_id = iseGranite[0].EQUIP_INST_ID || null;
    }

    // Hostname from card_flags (primary source from owner endpoint)
    if (!hostname && flags.CARD_HOSTNAME) {
        const hostnames = Array.isArray(flags.CARD_HOSTNAME) ? flags.CARD_HOSTNAME : [flags.CARD_HOSTNAME];
        hostname = hostnames[0] || null;
    }
    if (!hostname && qualys.length > 0) hostname = qualys[0].HOSTNAME || null;
    if (!hostname && ivanti.length > 0) hostname = ivanti[0].hostName || null;

    // ASN from card_flags
    const mgmt_ip_asn = flags.CARD_ASN || null;

    // CLLI → can be used as site hint
    if (!site_name && flags.CARD_CLLI) {
        site_name = flags.CARD_CLLI; // CLLI code, not full site name — user may need to map
    }

    // Equipment class — always S (Shelf) from CARD context
    const equipment_class = 'S';

    // Device ID → maps to SERIALNUMBER in Granite
    const serial_number = flags.CARD_DEVICE_ID || null;

    // Equip status from flags
    const equip_status = flags.status || null;

    // Vendor/model from card_flags
    let equip_template = null;
    if (flags.CARD_VENDOR_MODEL && Array.isArray(flags.CARD_VENDOR_MODEL) && flags.CARD_VENDOR_MODEL.length > 0) {
        const vm = flags.CARD_VENDOR_MODEL[0];
        equip_template = typeof vm === 'string' ? vm : (vm.vendor_model || null);
    }

    // Confirmed team from owner record
    const confirmed_team = asset.owner && asset.owner.confirmed
        ? asset.owner.confirmed.name : null;

    return {
        equip_inst_id: equip_inst_id ? String(equip_inst_id) : null,
        hostname,
        site_name,
        mgmt_ip_asn: mgmt_ip_asn ? String(mgmt_ip_asn) : null,
        responsible_team: responsible_team || confirmed_team || null,
        equipment_class,
        equip_template,
        equip_status,
        serial_number,
    };
}

module.exports = createCardApiRouter;
