const express = require('express');
const axios = require('axios');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const winston = require('winston');
const path = require('path');
const session = require('express-session');
const cookieParser = require('cookie-parser');
require('dotenv').config();

// Import geocoding routes
const geocodingRoutes = require('./routes/geocoding');

// Parse project and table IDs from view URL
function parseNocoDBUrl(url) {
    if (!url) return { projectId: null, tableId: null };
    
    // Pattern to match NocoDB URLs
    const patterns = [
        /#\/nc\/([^\/]+)\/([^\/\?#]+)/,  // matches #/nc/PROJECT_ID/TABLE_ID (dashboard URLs)
        /\/nc\/([^\/]+)\/([^\/\?#]+)/,   // matches /nc/PROJECT_ID/TABLE_ID
        /project\/([^\/]+)\/table\/([^\/\?#]+)/,  // alternative pattern
    ];
    
    for (const pattern of patterns) {
        const match = url.match(pattern);
        if (match) {
            return {
                projectId: match[1],
                tableId: match[2]
            };
        }
    }
    
    return { projectId: null, tableId: null };
}

// Add this helper function near the top of the file after the parseNocoDBUrl function
function syncGeoFields(data) {
    // If we have latitude and longitude but no Geo-Location, create it
    if (data.latitude && data.longitude && !data['Geo-Location']) {
        const lat = parseFloat(data.latitude);
        const lng = parseFloat(data.longitude);
        if (!isNaN(lat) && !isNaN(lng)) {
            data['Geo-Location'] = `${lat};${lng}`; // Use semicolon format for NocoDB GeoData
            data.geodata = `${lat};${lng}`; // Also update geodata for compatibility
        }
    }
    
    // If we have Geo-Location but no lat/lng, parse it
    else if (data['Geo-Location'] && (!data.latitude || !data.longitude)) {
        const geoLocation = data['Geo-Location'].toString();
        
        // Try semicolon-separated first
        let parts = geoLocation.split(';');
        if (parts.length === 2) {
            const lat = parseFloat(parts[0].trim());
            const lng = parseFloat(parts[1].trim());
            if (!isNaN(lat) && !isNaN(lng)) {
                data.latitude = lat;
                data.longitude = lng;
                data.geodata = `${lat};${lng}`;
                return data;
            }
        }
        
        // Try comma-separated
        parts = geoLocation.split(',');
        if (parts.length === 2) {
            const lat = parseFloat(parts[0].trim());
            const lng = parseFloat(parts[1].trim());
            if (!isNaN(lat) && !isNaN(lng)) {
                data.latitude = lat;
                data.longitude = lng;
                data.geodata = `${lat};${lng}`;
                // Normalize Geo-Location to semicolon format for NocoDB GeoData
                data['Geo-Location'] = `${lat};${lng}`;
            }
        }
    }
    
    return data;
}

// Auto-parse IDs if view URL is provided
if (process.env.NOCODB_VIEW_URL && (!process.env.NOCODB_PROJECT_ID || !process.env.NOCODB_TABLE_ID)) {
    const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_VIEW_URL);
    
    if (projectId && tableId) {
        process.env.NOCODB_PROJECT_ID = projectId;
        process.env.NOCODB_TABLE_ID = tableId;
        console.log(`Auto-parsed from URL - Project ID: ${projectId}, Table ID: ${tableId}`);
    }
}

// Auto-parse login sheet ID if URL is provided
let LOGIN_SHEET_ID = null;
if (process.env.NOCODB_LOGIN_SHEET) {
    // Check if it's a URL or just an ID
    if (process.env.NOCODB_LOGIN_SHEET.startsWith('http')) {
        const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_LOGIN_SHEET);
        if (projectId && tableId) {
            LOGIN_SHEET_ID = tableId;
            console.log(`Auto-parsed login sheet ID from URL: ${LOGIN_SHEET_ID}`);
        } else {
            console.error('Could not parse login sheet URL');
        }
    } else {
        // Assume it's already just the ID
        LOGIN_SHEET_ID = process.env.NOCODB_LOGIN_SHEET;
        console.log(`Using login sheet ID: ${LOGIN_SHEET_ID}`);
    }
}

// Configure logger
const logger = winston.createLogger({
    level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.Console({
            format: winston.format.simple()
        })
    ]
});

// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;

// Session configuration
app.use(cookieParser());

// Determine if we should use secure cookies based on environment and request
const isProduction = process.env.NODE_ENV === 'production';

// Cookie configuration function
const getCookieConfig = (req) => {
    const host = req?.get('host') || '';
    const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1') || host.match(/^\d+\.\d+\.\d+\.\d+/);
    
    const config = {
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000, // 24 hours
        sameSite: 'lax',
        secure: false, // Default to false
        domain: undefined // Default to no domain restriction
    };
    
    // Only set domain and secure for production non-localhost access
    if (isProduction && !isLocalhost && process.env.COOKIE_DOMAIN) {
        // Check if the request is coming from a subdomain of COOKIE_DOMAIN
        const cookieDomain = process.env.COOKIE_DOMAIN.replace(/^\./, ''); // Remove leading dot
        if (host.includes(cookieDomain)) {
            config.domain = process.env.COOKIE_DOMAIN;
            config.secure = true;
        }
    }
    
    return config;
};

app.use(session({
    secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production',
    resave: false,
    saveUninitialized: false,
    cookie: getCookieConfig(),
    name: 'nocodb-map-session',
    genid: (req) => {
        // Use a custom session ID generator to avoid conflicts
        return require('crypto').randomBytes(16).toString('hex');
    }
}));

// Security middleware
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"],
            scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"],
            imgSrc: ["'self'", "data:", "https://*.tile.openstreetmap.org"],
            connectSrc: ["'self'"]
        }
    }
}));

// CORS configuration
app.use(cors({
    origin: function(origin, callback) {
        // Allow requests with no origin (like mobile apps or curl requests)
        if (!origin) return callback(null, true);
        
        const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
        if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));

// Trust proxy for Cloudflare
app.set('trust proxy', true);

// Rate limiting with Cloudflare support
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
    keyGenerator: (req) => {
        // Use CF-Connecting-IP header if available (Cloudflare)
        return req.headers['cf-connecting-ip'] || 
               req.headers['x-forwarded-for']?.split(',')[0] || 
               req.ip;
    },
    standardHeaders: true,
    legacyHeaders: false,
});

const strictLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 20,
    keyGenerator: (req) => {
        return req.headers['cf-connecting-ip'] || 
               req.headers['x-forwarded-for']?.split(',')[0] || 
               req.ip;
    }
});

const authLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: process.env.NODE_ENV === 'production' ? 10 : 50, // Increase limit slightly
    message: 'Too many login attempts, please try again later.',
    keyGenerator: (req) => {
        return req.headers['cf-connecting-ip'] || 
               req.headers['x-forwarded-for']?.split(',')[0] || 
               req.ip;
    },
    standardHeaders: true,
    legacyHeaders: false,
});

// Middleware
app.use(express.json({ limit: '10mb' }));

// Authentication middleware
const requireAuth = (req, res, next) => {
    if (req.session && req.session.authenticated) {
        next();
    } else {
        if (req.xhr || req.headers.accept?.indexOf('json') > -1) {
            res.status(401).json({ success: false, error: 'Authentication required' });
        } else {
            res.redirect('/login.html');
        }
    }
};

// Serve login page without authentication
app.get('/login.html', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'login.html'));
});

// Auth routes (no authentication required)
app.post('/api/auth/login', authLimiter, async (req, res) => {
    try {
        // Log request details for debugging
        logger.info('Login attempt:', {
            email: req.body.email,
            ip: req.ip,
            cfIp: req.headers['cf-connecting-ip'],
            forwardedFor: req.headers['x-forwarded-for'],
            userAgent: req.headers['user-agent']
        });
        
        const { email } = req.body;
        
        if (!email) {
            return res.status(400).json({
                success: false,
                error: 'Email is required'
            });
        }
        
        // Validate email format
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(email)) {
            return res.status(400).json({
                success: false,
                error: 'Invalid email format'
            });
        }
        
        // Check if login sheet is configured
        if (!LOGIN_SHEET_ID) {
            logger.error('NOCODB_LOGIN_SHEET not configured or could not be parsed');
            return res.status(500).json({
                success: false,
                error: 'Authentication system not properly configured'
            });
        }
        
        // Fetch authorized emails from NocoDB
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`;
        
        logger.info(`Checking authentication for email: ${email}`);
        logger.debug(`Using login sheet API: ${url}`);
        
        const response = await axios.get(url, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN,
                'Content-Type': 'application/json'
            },
            params: {
                limit: 1000 // Adjust if you have more authorized users
            }
        });
        
        const users = response.data.list || [];
        
        // Check if email exists in the authorized users list
        const authorizedUser = users.find(user => 
            user.Email && user.Email.toLowerCase() === email.toLowerCase()
        );
        
        if (authorizedUser) {
            // Set session
            req.session.authenticated = true;
            req.session.userEmail = email;
            req.session.userName = authorizedUser.Name || email;
            
            // Force session save before sending response
            req.session.save((err) => {
                if (err) {
                    logger.error('Session save error:', err);
                    return res.status(500).json({
                        success: false,
                        error: 'Session error. Please try again.'
                    });
                }
                
                logger.info(`User authenticated: ${email}`);
                
                res.json({
                    success: true,
                    message: 'Login successful',
                    user: {
                        email: email,
                        name: req.session.userName
                    }
                });
            });
        } else {
            logger.warn(`Authentication failed for email: ${email}`);
            res.status(401).json({
                success: false,
                error: 'Email not authorized. Please contact an administrator.'
            });
        }
        
    } catch (error) {
        logger.error('Login error:', error.message);
        res.status(500).json({
            success: false,
            error: 'Authentication service error. Please try again later.'
        });
    }
});

app.get('/api/auth/check', (req, res) => {
    res.json({
        authenticated: req.session?.authenticated || false,
        user: req.session?.authenticated ? {
            email: req.session.userEmail,
            name: req.session.userName
        } : null
    });
});

app.post('/api/auth/logout', (req, res) => {
    req.session.destroy((err) => {
        if (err) {
            logger.error('Logout error:', err);
            return res.status(500).json({
                success: false,
                error: 'Logout failed'
            });
        }
        res.json({
            success: true,
            message: 'Logged out successfully'
        });
    });
});

// Add this after the /api/auth/check route
app.get('/api/debug/session', (req, res) => {
    res.json({
        sessionID: req.sessionID,
        session: req.session,
        cookies: req.cookies,
        authenticated: req.session?.authenticated || false
    });
});

// Serve static files with authentication for main app
app.use(express.static(path.join(__dirname, 'public'), {
    index: false // Don't serve index.html automatically
}));

// Protect main app routes
app.get('/', requireAuth, (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Add geocoding routes (protected)
app.use('/api/geocode', requireAuth, geocodingRoutes);

// Apply rate limiting to API routes
app.use('/api/', limiter);

// Health check endpoint (no auth required)
app.get('/health', (req, res) => {
    res.json({ 
        status: 'healthy', 
        timestamp: new Date().toISOString(),
        version: process.env.npm_package_version || '1.0.0'
    });
});

// Configuration validation endpoint (protected)
app.get('/api/config-check', requireAuth, (req, res) => {
    const config = {
        hasApiUrl: !!process.env.NOCODB_API_URL,
        hasApiToken: !!process.env.NOCODB_API_TOKEN,
        hasProjectId: !!process.env.NOCODB_PROJECT_ID,
        hasTableId: !!process.env.NOCODB_TABLE_ID,
        hasLoginSheet: !!LOGIN_SHEET_ID,
        projectId: process.env.NOCODB_PROJECT_ID,
        tableId: process.env.NOCODB_TABLE_ID,
        loginSheet: LOGIN_SHEET_ID,
        loginSheetConfigured: process.env.NOCODB_LOGIN_SHEET,
        nodeEnv: process.env.NODE_ENV
    };
    
    const isConfigured = config.hasApiUrl && config.hasApiToken && config.hasProjectId && config.hasTableId;
    
    res.json({
        configured: isConfigured,
        ...config
    });
});

// All other API routes require authentication
app.use('/api/*', requireAuth);

// Get all locations from NocoDB
app.get('/api/locations', async (req, res) => {
    try {
        const { limit = 1000, offset = 0, where } = req.query;
        
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`;
        
        const params = new URLSearchParams({
            limit,
            offset
        });
        
        if (where) {
            params.append('where', where);
        }
        
        logger.info(`Fetching locations from NocoDB: ${url}`);
        
        const response = await axios.get(`${url}?${params}`, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN,
                'Content-Type': 'application/json'
            },
            timeout: 10000 // 10 second timeout
        });
        
        // Process locations to ensure they have required fields
        const locations = response.data.list || [];
        
        const validLocations = locations.filter(loc => {
            // Apply geo field synchronization to each location
            loc = syncGeoFields(loc);
            
            // Check if location has valid coordinates
            if (loc.latitude && loc.longitude) {
                return true;
            }
            
            // Try to parse from geodata column (semicolon-separated)
            if (loc.geodata && typeof loc.geodata === 'string') {
                const parts = loc.geodata.split(';');
                if (parts.length === 2) {
                    loc.latitude = parseFloat(parts[0]);
                    loc.longitude = parseFloat(parts[1]);
                    return !isNaN(loc.latitude) && !isNaN(loc.longitude);
                }
            }
            
            // Try to parse from Geo-Location column (semicolon-separated first, then comma)
            if (loc['Geo-Location'] && typeof loc['Geo-Location'] === 'string') {
                // Try semicolon first (as we see in the data)
                let parts = loc['Geo-Location'].split(';');
                if (parts.length === 2) {
                    loc.latitude = parseFloat(parts[0].trim());
                    loc.longitude = parseFloat(parts[1].trim());
                    if (!isNaN(loc.latitude) && !isNaN(loc.longitude)) {
                        return true;
                    }
                }
                
                // Fallback to comma-separated
                parts = loc['Geo-Location'].split(',');
                if (parts.length === 2) {
                    loc.latitude = parseFloat(parts[0].trim());
                    loc.longitude = parseFloat(parts[1].trim());
                    return !isNaN(loc.latitude) && !isNaN(loc.longitude);
                }
            }
            
            return false;
        });
        
        logger.info(`Retrieved ${validLocations.length} valid locations out of ${locations.length} total`);
        
        res.json({
            success: true,
            count: validLocations.length,
            total: response.data.pageInfo?.totalRows || validLocations.length,
            locations: validLocations
        });
        
    } catch (error) {
        logger.error('Error fetching locations:', error.message);
        
        if (error.response) {
            // NocoDB API error
            res.status(error.response.status).json({
                success: false,
                error: 'Failed to fetch data from NocoDB',
                details: error.response.data
            });
        } else if (error.code === 'ECONNABORTED') {
            // Timeout
            res.status(504).json({
                success: false,
                error: 'Request timeout'
            });
        } else {
            // Other errors
            res.status(500).json({
                success: false,
                error: 'Internal server error'
            });
        }
    }
});

// Get single location by ID
app.get('/api/locations/:id', async (req, res) => {
    try {
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${req.params.id}`;
        
        const response = await axios.get(url, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN
            }
        });
        
        res.json({
            success: true,
            location: response.data
        });
        
    } catch (error) {
        logger.error(`Error fetching location ${req.params.id}:`, error.message);
        res.status(error.response?.status || 500).json({
            success: false,
            error: 'Failed to fetch location'
        });
    }
});

// Create new location
app.post('/api/locations', strictLimiter, async (req, res) => {
    try {
        let locationData = { ...req.body };
        
        // Sync geo fields before validation
        locationData = syncGeoFields(locationData);
        
        const { latitude, longitude, ...additionalData } = locationData;
        
        // Validate coordinates
        if (!latitude || !longitude) {
            return res.status(400).json({
                success: false,
                error: 'Latitude and longitude are required'
            });
        }
        
        const lat = parseFloat(latitude);
        const lng = parseFloat(longitude);
        
        if (isNaN(lat) || isNaN(lng)) {
            return res.status(400).json({
                success: false,
                error: 'Invalid coordinate values'
            });
        }
        
        if (lat < -90 || lat > 90) {
            return res.status(400).json({
                success: false,
                error: 'Latitude must be between -90 and 90'
            });
        }
        
        if (lng < -180 || lng > 180) {
            return res.status(400).json({
                success: false,
                error: 'Longitude must be between -180 and 180'
            });
        }
        
        // Check bounds if configured
        if (process.env.BOUND_NORTH) {
            const bounds = {
                north: parseFloat(process.env.BOUND_NORTH),
                south: parseFloat(process.env.BOUND_SOUTH),
                east: parseFloat(process.env.BOUND_EAST),
                west: parseFloat(process.env.BOUND_WEST)
            };
            
            if (lat > bounds.north || lat < bounds.south || 
                lng > bounds.east || lng < bounds.west) {
                return res.status(400).json({
                    success: false,
                    error: 'Location is outside allowed bounds'
                });
            }
        }
        
        // Format geodata in both formats for compatibility
        const geodata = `${lat};${lng}`;
        const geoLocation = `${lat};${lng}`; // Use semicolon format for NocoDB GeoData column
        
        // Prepare data for NocoDB
        const finalData = {
            geodata,
            'Geo-Location': geoLocation,
            latitude: lat,
            longitude: lng,
            ...additionalData,
            created_at: new Date().toISOString(),
            created_by: req.session.userEmail // Track who created the location
        };
        
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`;
        
        logger.info('Creating new location:', { lat, lng });
        
        const response = await axios.post(url, finalData, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN,
                'Content-Type': 'application/json'
            }
        });
        
        logger.info('Location created successfully:', response.data.id);
        
        res.status(201).json({
            success: true,
            location: response.data
        });
        
    } catch (error) {
        logger.error('Error creating location:', error.message);
        
        if (error.response) {
            res.status(error.response.status).json({
                success: false,
                error: 'Failed to save location to NocoDB',
                details: error.response.data
            });
        } else {
            res.status(500).json({
                success: false,
                error: 'Internal server error'
            });
        }
    }
});

// Update location
app.put('/api/locations/:id', strictLimiter, async (req, res) => {
    try {
        let updateData = { ...req.body };
        
        // Sync geo fields
        updateData = syncGeoFields(updateData);
        
        updateData.updated_at = new Date().toISOString();
        updateData.updated_by = req.session.userEmail; // Track who updated
        
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${req.params.id}`;
        
        const response = await axios.patch(url, updateData, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN,
                'Content-Type': 'application/json'
            }
        });
        
        res.json({
            success: true,
            location: response.data
        });
        
    } catch (error) {
        logger.error(`Error updating location ${req.params.id}:`, error.message);
        res.status(error.response?.status || 500).json({
            success: false,
            error: 'Failed to update location'
        });
    }
});

// Delete location
app.delete('/api/locations/:id', strictLimiter, async (req, res) => {
    try {
        const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${req.params.id}`;
        
        await axios.delete(url, {
            headers: {
                'xc-token': process.env.NOCODB_API_TOKEN
            }
        });
        
        logger.info(`Location ${req.params.id} deleted by ${req.session.userEmail}`);
        
        res.json({
            success: true,
            message: 'Location deleted successfully'
        });
        
    } catch (error) {
        logger.error(`Error deleting location ${req.params.id}:`, error.message);
        res.status(error.response?.status || 500).json({
            success: false,
            error: 'Failed to delete location'
        });
    }
});

// Error handling middleware
app.use((err, req, res, next) => {
    logger.error('Unhandled error:', err);
    res.status(500).json({
        success: false,
        error: 'Internal server error'
    });
});

// Start server
app.listen(PORT, () => {
    logger.info(`
    ╔════════════════════════════════════════╗
    ║      NocoDB Map Viewer Server          ║
    ╠════════════════════════════════════════╣
    ║  Status: Running                       ║
    ║  Port: ${PORT}                            ║
    ║  Environment: ${process.env.NODE_ENV || 'development'}          ║
    ║  Project ID: ${process.env.NOCODB_PROJECT_ID}             ║
    ║  Table ID: ${process.env.NOCODB_TABLE_ID}               ║
    ║  Login Sheet: ${LOGIN_SHEET_ID || 'Not Configured'}              ║
    ║  Time: ${new Date().toISOString()}    ║
    ╚════════════════════════════════════════╝
    `);
});

// Graceful shutdown
process.on('SIGTERM', () => {
    logger.info('SIGTERM signal received: closing HTTP server');
    app.close(() => {
        logger.info('HTTP server closed');
        process.exit(0);
    });
});

