#!/usr/bin/env node
'use strict';

const https = require('https');
const { execSync } = require('child_process');
const { URL } = require('url');

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------

const args = process.argv.slice(2);
const FLAG_FORCE = args.includes('--force');
const FLAG_DRY_RUN = args.includes('--dry-run') || !FLAG_FORCE;
const FLAG_VERBOSE = args.includes('--verbose');

if (args.includes('--help') || args.includes('-h')) {
    console.log(`Usage: node sync-sc-from-syncro.js [--dry-run] [--force] [--verbose]

Options:
  --dry-run   List what would be updated without making SC API calls (default)
  --force     Actually perform the ScreenConnect updates
  --verbose   Show detailed output for each asset
  --help      Show this help message

Without --force, the script runs in dry-run mode.`);
    process.exit(0);
}

// ---------------------------------------------------------------------------
// Credential loading via vault
// ---------------------------------------------------------------------------

const VAULT_PATH = process.env.VAULT_PATH || 'D:/vault';

function vaultGet(file, field) {
    try {
        const result = execSync(
            `bash "${VAULT_PATH}/scripts/vault.sh" get-field "${file}" "${field}"`,
            { encoding: 'utf8', timeout: 30000 }
        );
        return result.trim();
    } catch (err) {
        console.error(`[ERROR] Failed to read vault field "${field}" from "${file}": ${err.message}`);
        process.exit(1);
    }
}

let SYNCRO_API_KEY;
let SC_SECRET;

function loadCredentials() {
    console.log('Loading credentials from vault...');
    SYNCRO_API_KEY = vaultGet('msp-tools/syncro.sops.yaml', 'credentials.credential');
    SC_SECRET = vaultGet('msp-tools/screenconnect.sops.yaml', 'credentials.api_secret');

    if (!SYNCRO_API_KEY) {
        console.error('[ERROR] Syncro API key is empty.');
        process.exit(1);
    }
    if (!SC_SECRET) {
        console.error('[ERROR] ScreenConnect API secret is empty.');
        process.exit(1);
    }
    console.log('Credentials loaded successfully.');
}

// ---------------------------------------------------------------------------
// HTTP helpers (built-in https module)
// ---------------------------------------------------------------------------

/**
 * Make an HTTPS request and return the parsed JSON response.
 * @param {object} options
 * @param {string} options.url - Full URL
 * @param {string} [options.method='GET'] - HTTP method
 * @param {object} [options.headers={}] - Request headers
 * @param {string|null} [options.body=null] - Request body (string)
 * @returns {Promise<{statusCode: number, data: any}>}
 */
function httpsRequest({ url, method = 'GET', headers = {}, body = null }) {
    return new Promise((resolve, reject) => {
        const parsed = new URL(url);
        const reqOptions = {
            hostname: parsed.hostname,
            port: parsed.port || 443,
            path: parsed.pathname + parsed.search,
            method,
            headers,
        };

        const req = https.request(reqOptions, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                const raw = Buffer.concat(chunks).toString('utf8');
                let data;
                try {
                    data = JSON.parse(raw);
                } catch (_) {
                    data = raw;
                }
                resolve({ statusCode: res.statusCode, data });
            });
        });

        req.on('error', (err) => reject(err));
        req.setTimeout(30000, () => {
            req.destroy(new Error('Request timed out'));
        });

        if (body !== null) {
            const buf = Buffer.from(body, 'utf8');
            req.setHeader('Content-Length', buf.length);
            req.write(buf);
        }
        req.end();
    });
}

/**
 * Sleep for a given number of milliseconds.
 * @param {number} ms
 * @returns {Promise<void>}
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

// ---------------------------------------------------------------------------
// Syncro API - paginate all assets
// ---------------------------------------------------------------------------

/**
 * Fetch all customer assets from Syncro, paginating until no more results.
 * @returns {Promise<Array<{scGuid: string, company: string, deviceName: string, customerId: number}>>}
 */
async function fetchAllSyncroAssets() {
    const baseUrl = 'https://computerguru.syncromsp.com/api/v1/customer_assets';
    const perPage = 100;
    let page = 1;
    const results = [];
    let totalRaw = 0;

    console.log('Fetching Syncro assets...');

    while (true) {
        const url = `${baseUrl}?page=${page}&per_page=${perPage}`;
        if (FLAG_VERBOSE) {
            console.log(`  Fetching page ${page}...`);
        }

        let response;
        try {
            response = await httpsRequest({
                url,
                method: 'GET',
                headers: {
                    'Authorization': SYNCRO_API_KEY,
                    'Accept': 'application/json',
                },
            });
        } catch (err) {
            console.error(`[ERROR] Syncro API request failed on page ${page}: ${err.message}`);
            process.exit(1);
        }

        if (response.statusCode !== 200) {
            console.error(`[ERROR] Syncro API returned status ${response.statusCode} on page ${page}.`);
            if (typeof response.data === 'string') {
                console.error(`  Response: ${response.data.substring(0, 500)}`);
            }
            process.exit(1);
        }

        const assets = response.data.assets || response.data;

        if (!Array.isArray(assets) || assets.length === 0) {
            if (FLAG_VERBOSE) {
                console.log(`  Page ${page} returned 0 assets, done paginating.`);
            }
            break;
        }

        totalRaw += assets.length;

        for (const asset of assets) {
            const props = asset.properties || {};
            const scGuid = props['ScreenConnect GUID'] || '';
            if (!scGuid) {
                continue;
            }

            const customer = asset.customer || {};
            results.push({
                scGuid,
                company: customer.business_then_name || '',
                deviceName: asset.name || 'Unknown',
                customerId: customer.id || 0,
                deviceType: (props.form_factor || '').replace(/^Physical /, ''),
            });
        }

        if (FLAG_VERBOSE) {
            console.log(`  Page ${page}: ${assets.length} assets (${results.length} with SC GUID so far)`);
        }

        page++;
    }

    console.log(`Fetched ${totalRaw} total assets, ${results.length} have ScreenConnect GUIDs.`);
    return { totalRaw, assets: results };
}

// ---------------------------------------------------------------------------
// ScreenConnect API - read session custom properties
// ---------------------------------------------------------------------------

/**
 * Read current session custom properties from ScreenConnect to check tagging.
 * Uses the same extension endpoint with GetSessionDetails or similar.
 *
 * Note: ScreenConnect does not have a clean REST API for reading session
 * properties by GUID through the extension endpoint. We attempt to read
 * session details. If reading fails or is unsupported, we return null
 * and let the caller decide whether to update.
 *
 * @param {string} scGuid
 * @returns {Promise<string[]|null>} Array of custom property values, or null if unreadable
 */
async function readScSessionProperties(scGuid) {
    const url = 'https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/GetSessionDetails';

    try {
        const response = await httpsRequest({
            url,
            method: 'POST',
            headers: {
                'CTRLAuthHeader': SC_SECRET,
                'Content-Type': 'application/json',
                'Origin': 'https://computerguru.screenconnect.com',
            },
            body: JSON.stringify([scGuid]),
        });

        if (response.statusCode === 200 && response.data) {
            // The response structure may vary; attempt to extract custom properties
            const session = response.data;
            if (Array.isArray(session.CustomPropertyValues)) {
                return session.CustomPropertyValues;
            }
            if (Array.isArray(session)) {
                const first = session[0];
                if (first && Array.isArray(first.CustomPropertyValues)) {
                    return first.CustomPropertyValues;
                }
            }
        }
    } catch (_) {
        // Reading failed - will attempt update anyway (unless --force is off)
    }

    return null;
}

// ---------------------------------------------------------------------------
// ScreenConnect API - update session custom properties
// ---------------------------------------------------------------------------

/**
 * Update ScreenConnect session custom properties for a given GUID.
 * @param {string} scGuid
 * @param {string} company
 * @param {string} site
 * @returns {Promise<{success: boolean, error?: string}>}
 */
async function updateScSession(scGuid, company, deviceType) {
    const url = 'https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/UpdateSessionCustomProperties';

    // CP1=Company, CP2=Site (blank), CP3=Department (blank), CP4=Device Type, CP5=Tag, CP6-8=blank
    const bodyPayload = [
        scGuid,
        [company, '', '', deviceType, 'Syncro-Matched', '', '', ''],
    ];

    try {
        const response = await httpsRequest({
            url,
            method: 'POST',
            headers: {
                'CTRLAuthHeader': SC_SECRET,
                'Content-Type': 'application/json',
                'Origin': 'https://computerguru.screenconnect.com',
            },
            body: JSON.stringify(bodyPayload),
        });

        if (response.statusCode >= 200 && response.statusCode < 300) {
            return { success: true };
        }

        const detail = typeof response.data === 'string'
            ? response.data.substring(0, 200)
            : JSON.stringify(response.data).substring(0, 200);
        return { success: false, error: `HTTP ${response.statusCode}: ${detail}` };
    } catch (err) {
        return { success: false, error: err.message };
    }
}

// ---------------------------------------------------------------------------
// Main processing
// ---------------------------------------------------------------------------

async function main() {
    console.log('=== SC-Syncro Session Sync ===');
    console.log(`Mode: ${FLAG_FORCE ? 'LIVE (--force)' : 'DRY-RUN'}`);
    console.log('');

    loadCredentials();
    console.log('');

    const { totalRaw, assets } = await fetchAllSyncroAssets();
    console.log('');

    const stats = {
        totalSyncroAssets: totalRaw,
        assetsWithScGuid: assets.length,
        alreadyTagged: 0,
        updated: 0,
        errors: 0,
        wouldUpdate: 0,
    };

    for (let i = 0; i < assets.length; i++) {
        const asset = assets[i];
        const shortGuid = asset.scGuid.length > 12
            ? asset.scGuid.substring(0, 12) + '...'
            : asset.scGuid;

        // Check if already tagged
        const currentProps = await readScSessionProperties(asset.scGuid);
        if (currentProps !== null && currentProps.length >= 5 && currentProps[4] === 'Syncro-Matched') {
            stats.alreadyTagged++;
            if (FLAG_VERBOSE) {
                console.log(`[SKIP] ${asset.deviceName} (${shortGuid}) - already tagged`);
            }
            await sleep(100);
            continue;
        }

        if (FLAG_DRY_RUN && !FLAG_FORCE) {
            stats.wouldUpdate++;
            if (FLAG_VERBOSE) {
                console.log(`[DRY-RUN] ${asset.deviceName} (${shortGuid}) -> Company: "${asset.company}", Type: "${asset.deviceType}"`);
            }
            await sleep(100);
            continue;
        }

        // Perform the update
        const result = await updateScSession(asset.scGuid, asset.company, asset.deviceType);

        if (result.success) {
            stats.updated++;
            if (FLAG_VERBOSE) {
                console.log(`[UPDATE] ${asset.deviceName} (${shortGuid}) -> Company: "${asset.company}", Type: "${asset.deviceType}"`);
            }
        } else {
            stats.errors++;
            if (FLAG_VERBOSE) {
                console.log(`[ERROR] ${asset.deviceName} (${shortGuid}): ${result.error}`);
            }
        }

        // Rate limiting: 200ms between SC API calls
        await sleep(200);

        // Progress indicator for non-verbose mode
        if (!FLAG_VERBOSE && (i + 1) % 50 === 0) {
            console.log(`  Progress: ${i + 1}/${assets.length} processed...`);
        }
    }

    // Print summary
    console.log('');
    console.log('SC-Syncro Sync Results:');
    console.log(`  Total Syncro assets: ${stats.totalSyncroAssets}`);
    console.log(`  Assets with SC GUID: ${stats.assetsWithScGuid}`);
    console.log(`  Already tagged (skipped): ${stats.alreadyTagged}`);

    if (FLAG_DRY_RUN && !FLAG_FORCE) {
        console.log(`  Would update: ${stats.wouldUpdate}`);
        console.log('');
        console.log('Run with --force to apply updates.');
    } else {
        console.log(`  Updated: ${stats.updated}`);
        console.log(`  Errors: ${stats.errors}`);
    }
}

main().catch((err) => {
    console.error(`[FATAL] Unhandled error: ${err.message}`);
    if (err.stack) {
        console.error(err.stack);
    }
    process.exit(1);
});
