const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const http = require('http');
const os = require('os');
const configManager = require('./config-manager');

const app = express();
const PORT = process.env.PORT || configManager.getPort();

app.use(cors());
app.use(express.json());

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// Port map and state file locators for apps (Assistant instances are discovered separately).
const APPS = [
    { id: 'suite',        name: 'Suite',        port: 19001, stateFile: '.ollie-suite/state.json' },
    { id: 'mail',         name: 'Mail',         port: 19003, stateFile: '.ollie-mail/state.json' },
    { id: 'intelligence', name: 'Intelligence', port: 19004, stateFile: '.ollie-intelligence/state.json' },
    { id: 'browser',      name: 'Browser',      port: 19005, stateFile: '.ollie-browser/state.json' },
    { id: 'explorer',     name: 'Explorer',     port: 19006, stateFile: '.ollie-explorer/state.json' },
    { id: 'media',        name: 'Media',        port: 19007, stateFile: '.ollie-media/state.json' },
    { id: 'catalog',      name: 'Catalog',      port: 19008, stateFile: '.ollie-catalog/state.json' },
    { id: 'notepad',      name: 'Notepad',      port: 19010, stateFile: '.ollie-notepad/state.json' },
    { id: 'camera',       name: 'Camera',       port: 19011, stateFile: '.ollie-camera/state.json' },
    { id: 'cosmos',       name: 'Cosmos',       port: 19013, stateFile: '.ollie-cosmos/state.json' },
];

function parseAssistantScanPorts() {
    const ports = new Set();
    ports.add(19002);
    const extra = String(process.env.OLLIE_HUB_ASSISTANT_PORTS || '')
        .split(',')
        .map((s) => parseInt(s.trim(), 10))
        .filter((n) => Number.isFinite(n) && n > 0 && n < 65536);
    extra.forEach((p) => ports.add(p));
    const scanEnd = parseInt(process.env.OLLIE_HUB_ASSISTANT_SCAN_END || '19200', 10);
    const end = Number.isFinite(scanEnd) ? Math.min(scanEnd, 65535) : 19200;
    for (let p = 19010; p <= end; p++) ports.add(p);
    return [...ports].sort((a, b) => a - b);
}

function sanitizeAssistantIdentityLabel(s) {
    return String(s || '').replace(/[<>&\"']/g, '').trim().slice(0, 120);
}

function probeAssistantRow(port) {
    return new Promise((resolve) => {
        const req = http.get(`http://127.0.0.1:${port}/api/status`, { timeout: 350 }, (res) => {
            let raw = '';
            res.on('data', (d) => {
                raw += d;
            });
            res.on('end', () => {
                try {
                    const j = JSON.parse(raw);
                    if (j.status !== 'online') return resolve(null);
                    const legacyAssistant = port === 19002 && !j.app;
                    const modernAssistant = j.app === 'ollie-assistant';
                    if (!legacyAssistant && !modernAssistant) return resolve(null);
                    const id = j.identity && j.identity.id ? String(j.identity.id) : '';
                    const rawName = j.identity && j.identity.name ? String(j.identity.name) : '';
                    const identityName = sanitizeAssistantIdentityLabel(rawName);
                    const displayName = identityName
                        ? `Assistant — ${identityName}`
                        : `Assistant (${port})`;
                    resolve({
                        id: `assistant-${port}`,
                        name: displayName,
                        port,
                        identity_name: identityName || null,
                        identity_id: id || null,
                    });
                } catch {
                    resolve(null);
                }
            });
        });
        req.on('error', () => resolve(null));
        req.setTimeout(350, () => {
            req.destroy();
            resolve(null);
        });
    });
}

function probeAssistantActivated(port) {
    return new Promise((resolve) => {
        const req = http.get(`http://127.0.0.1:${port}/api/settings/state`, { timeout: 400 }, (res) => {
            let raw = '';
            res.on('data', (d) => {
                raw += d;
            });
            res.on('end', () => {
                try {
                    const state = JSON.parse(raw);
                    const keys = Object.keys(state);
                    for (const key of keys) {
                        if (key.endsWith('-license-verified') && (state[key] === true || state[key] === 'true')) {
                            return resolve(true);
                        }
                    }
                    for (const key of keys) {
                        if (
                            key.endsWith('-verification') &&
                            state[key] &&
                            (state[key].key || state[key].licenseKey)
                        ) {
                            return resolve(true);
                        }
                    }
                    resolve(false);
                } catch {
                    resolve(false);
                }
            });
        });
        req.on('error', () => resolve(false));
        req.setTimeout(400, () => {
            req.destroy();
            resolve(false);
        });
    });
}

async function discoverAssistantInstances() {
    const ports = parseAssistantScanPorts();
    const chunk = 48;
    const found = [];
    for (let i = 0; i < ports.length; i += chunk) {
        const batch = ports.slice(i, i + chunk);
        const rows = await Promise.all(batch.map((p) => probeAssistantRow(p)));
        for (const row of rows) {
            if (row) found.push(row);
        }
    }
    const withLic = await Promise.all(
        found.map(async (row) => ({
            ...row,
            activated: await probeAssistantActivated(row.port),
        })),
    );
    return withLic;
}

// Helper to check if a port is responding
async function checkPortOnline(port) {
    return new Promise((resolve) => {
        let settled = false;
        const done = (val) => { if (!settled) { settled = true; resolve(val); } };

        const req = http.get(`http://127.0.0.1:${port}/api/system/version`, { timeout: 1000 }, (res) => {
            res.resume();
            done(res.statusCode === 200 || res.statusCode === 404);
        });

        req.on('error', () => {
            if (settled) return;
            const fallbackReq = http.get(`http://127.0.0.1:${port}/`, { timeout: 1000 }, (resFallback) => {
                resFallback.resume();
                done(true);
            });
            fallbackReq.on('error', () => done(false));
            fallbackReq.setTimeout(1000, () => { fallbackReq.destroy(); done(false); });
        });

        req.setTimeout(1000, () => { req.destroy(); done(false); });
    });
}

// Helper to check activation from state file
function checkActivation(appDef) {
    try {
        const statePath = path.join(os.homedir(), appDef.stateFile);
        if (!fs.existsSync(statePath)) return false;
        
        const stateStr = fs.readFileSync(statePath, 'utf8');
        const state = JSON.parse(stateStr);
        
        // Ollie uses `{namespace}-license-verified` and `{namespace}-verification` patterns
        const keys = Object.keys(state);
        for (const key of keys) {
            if (key.endsWith('-license-verified') && (state[key] === true || state[key] === 'true')) return true;
        }

        for (const key of keys) {
            if (key.endsWith('-verification') && state[key] && (state[key].key || state[key].licenseKey)) return true;
        }

        return false;
    } catch (e) {
        return false;
    }
}

// Hub API endpoint to get ecosystem status
app.get('/api/hub/status', async (req, res) => {
    const baseStatuses = await Promise.all(
        APPS.map(async (appDef) => {
            const isOnline = await checkPortOnline(appDef.port);
            const isActivated = checkActivation(appDef);

            return {
                id: appDef.id,
                name: appDef.name,
                port: appDef.port,
                online: isOnline,
                activated: isActivated,
                identity_name: null,
                identity_id: null,
            };
        }),
    );

    let assistantStatuses = [];
    try {
        const instances = await discoverAssistantInstances();
        assistantStatuses = instances.map((a) => ({
            id: a.id,
            name: a.name,
            port: a.port,
            online: true,
            activated: a.activated,
            identity_name: a.identity_name,
            identity_id: a.identity_id,
        }));
    } catch (e) {
        console.error('[hub] Assistant discovery failed:', e.message || e);
    }

    res.json({ success: true, apps: [...baseStatuses, ...assistantStatuses] });
});


const PM_API_URL = process.env.OLLIE_PM_URL || 'https://pm.costa-and-associates.com';

app.get('/api/system/pm-url', (req, res) => {
    res.json({ url: PM_API_URL });
});

// App Version Configuration Route
app.get('/api/system/version', (req, res) => {
    try {
        const packageJsonPath = path.join(__dirname, '..', 'package.json');
        if (fs.existsSync(packageJsonPath)) {
            const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
            res.json({ success: true, version: packageJson.version || '0.0.0' });
        } else {
            res.status(404).json({ success: false, error: 'package.json not found' });
        }
    } catch (e) {
        res.status(500).json({ success: false, error: 'Failed to read version' });
    }
});

// Settings / State Sync Routes
app.get('/api/settings/state', (req, res) => {
    const stateFile = path.join(configManager.configDir, 'state.json');
    try {
        if (fs.existsSync(stateFile)) {
            const state = fs.readFileSync(stateFile, 'utf8');
            res.json(JSON.parse(state));
        } else {
            res.json({});
        }
    } catch (e) {
        res.status(500).json({ error: 'Failed to read state' });
    }
});

app.post('/api/settings/sync', (req, res) => {
    const stateFile = path.join(configManager.configDir, 'state.json');
    try {
        const newState = req.body;
        let currentState = {};
        if (fs.existsSync(stateFile)) {
            currentState = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
        }
        const updatedState = { ...currentState, ...newState };
        fs.writeFileSync(stateFile, JSON.stringify(updatedState, null, 2), 'utf8');
        configManager.config = updatedState;
        res.json({ success: true });
    } catch (e) {
        console.error('Failed to sync state:', e);
        res.status(500).json({ error: 'Failed to sync state' });
    }
});

const OLLIE_WEBPAGE_URL = process.env.OLLIE_WEBPAGE_URL || 'https://olliesystem.com';
const APP_CODE = 'ollie-hub';

// Update check — proxies olliesystem.com /api/versions/<app_code>.
app.post('/api/system/check-updates', async (req, res) => {
    try {
        const data = await fetchOllieVersion(APP_CODE);
        res.json({
            success: true,
            app_code: data.app_code || APP_CODE,
            name: data.name,
            version: data.version,
            released_at: data.released_at,
            download_url: data.download_url,
            release_notes_url: data.release_notes_url,
        });
    } catch (e) {
        res.json({ success: false, error: e.message });
    }
});

function fetchOllieVersion(appCode) {
    const url = `${OLLIE_WEBPAGE_URL}/api/versions/${appCode}`;
    const lib = url.startsWith('https') ? require('https') : require('http');
    return new Promise((resolve, reject) => {
        const r = lib.get(url, { timeout: 5000 }, (resp) => {
            let raw = '';
            resp.on('data', d => (raw += d));
            resp.on('end', () => {
                if (resp.statusCode && resp.statusCode >= 400) {
                    return reject(new Error(`versions endpoint returned ${resp.statusCode}`));
                }
                try {
                    const data = JSON.parse(raw);
                    if (data && data.success === false) {
                        return reject(new Error(data.error || 'versions endpoint returned success:false'));
                    }
                    resolve(data || {});
                } catch (e) {
                    reject(new Error('invalid JSON from versions endpoint'));
                }
            });
        });
        r.on('error', reject);
        r.setTimeout(5000, () => {
            r.destroy();
            reject(new Error('versions endpoint timeout'));
        });
    });
}

// Fallback to index.html for SPA routing
app.get('*', (req, res) => {
    // SPA fallback. Only return index.html for browser navigations
    // (HTML accept, no file extension). Asset paths (/components/*.js,
    // /vendor/*.css, etc.) get a real 404. Without this, a missing asset
    // silently returns the HTML shell, the browser parses it as JS, and
    // dies with "Uncaught SyntaxError: Unexpected token '<'".
    if (req.path.startsWith('/api')) {
        return res.status(404).json({ error: 'API endpoint not found' });
    }
    const looksLikeAsset = /\.[a-zA-Z0-9]{1,5}$/.test(req.path);
    const wantsHtml = (req.headers.accept || '').includes('text/html');
    if (looksLikeAsset || !wantsHtml) {
        return res.status(404).type('text/plain').send('Not Found');
    }
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

if (require.main === module) {
    app.listen(PORT, '127.0.0.1', () => {
        console.log(`Hub server running at http://localhost:${PORT}`);
    });
}

module.exports = app;
