const express = require('express');
const fs = require('fs');
const http = require('http');
const https = require('https');
const tls = require('tls');
const validatorLib = require('validator');
const { APP, REGEX, TIMEOUTS } = require('../src/utilities/constants');
const { validateServiceConfig, isValidPort } = require('../src/security/input-validator');
const { exists } = require('../src/utilities/fs-helpers');
const { paginate, parsePaginationParams } = require('../src/utilities/pagination');
const { ValidationError, NotFoundError, ConflictError } = require('../src/utilities/errors');
const { resolveServiceUrl } = require('../src/utilities/url-resolver');
const { success, error: errorResponse } = require('../src/utils/responses');
const platformPaths = require('../platform-paths');

/**
 * Services route factory
 * @param {Object} deps - Explicit dependencies
 * @param {Object} deps.servicesStateManager - State manager for services.json
 * @param {Object} deps.credentialManager - Credential storage manager
 * @param {Object} deps.siteConfig - Site configuration
 * @param {Function} deps.buildServiceUrl - URL builder function
 * @param {Function} deps.buildDomain - Domain builder function
 * @param {Function} deps.fetchT - Fetch wrapper with timeout
 * @param {Function} deps.asyncHandler - Async route handler wrapper
 * @param {string} deps.SERVICES_FILE - Path to services.json
 * @param {Object} deps.log - Logger instance
 * @param {Function} deps.safeErrorMessage - Safe error message extractor
 * @param {Function} deps.resyncHealthChecker - Health checker resync function
 * @param {Object} deps.caddy - Caddy management interface
 * @param {Object} deps.dns - DNS management interface
 * @returns {express.Router}
 */
module.exports = function({
  servicesStateManager,
  credentialManager,
  siteConfig,
  buildServiceUrl,
  buildDomain,
  fetchT,
  asyncHandler,
  SERVICES_FILE,
  log,
  safeErrorMessage,
  resyncHealthChecker,
  caddy,
  dns
}) {
  const router = express.Router();
  const CA_CERT_PATH = process.env.CA_CERT_PATH || platformPaths.pkiRootCert;
  const PROBE_CONCURRENCY = 6;
  let probeHttpsAgent;

  try {
    const caCert = fs.readFileSync(CA_CERT_PATH);
    probeHttpsAgent = new https.Agent({ ca: [...tls.rootCertificates, caCert] });
  } catch (_) {
    // CA cert not available — trust self-signed certs so probes still work
    probeHttpsAgent = new https.Agent({ rejectUnauthorized: false });
  }

  function isServiceUp(statusCode) {
    return (statusCode >= 200 && statusCode < 400) || statusCode === 401 || statusCode === 403;
  }

  async function loadServicesList() {
    if (!await exists(SERVICES_FILE)) return [];
    const data = await servicesStateManager.read();
    return Array.isArray(data) ? data : data.services || [];
  }

  function resolveProbeUrl(id, service) {
    return resolveServiceUrl(id, service, siteConfig, buildServiceUrl);
  }

  const PROBE_TIMEOUT = 3000; // 3s — covers DNS + connect + response

  function requestStatusCode(url, method) {
    const parsed = new URL(url);
    const isHttps = parsed.protocol === 'https:';
    const lib = isHttps ? https : http;

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        req.destroy();
        reject(new Error('Timeout'));
      }, PROBE_TIMEOUT);

      const req = lib.request({
        hostname: parsed.hostname,
        port: parsed.port || (isHttps ? 443 : 80),
        path: parsed.pathname + parsed.search,
        method,
        agent: isHttps ? probeHttpsAgent : undefined,
        headers: { 'User-Agent': APP.USER_AGENTS.PROBE },
      }, (response) => {
        clearTimeout(timer);
        response.resume();
        resolve(response.statusCode || 0);
      });

      req.on('error', (err) => {
        clearTimeout(timer);
        reject(err);
      });
      req.end();
    });
  }

  async function probeViaPylon(targetUrl) {
    const pylonConfig = siteConfig?.pylon;
    if (!pylonConfig?.url) return null;
    try {
      const probeUrl = `${pylonConfig.url}/probe?url=${encodeURIComponent(targetUrl)}`;
      const headers = {};
      if (pylonConfig.key) headers['x-pylon-key'] = pylonConfig.key;
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), TIMEOUTS.HTTP_DEFAULT);
      const response = await fetchT(probeUrl, { method: 'GET', signal: controller.signal, headers });
      clearTimeout(timeout);
      if (!response.ok) return null;
      const data = await response.json();
      return data;
    } catch (_) {
      return null;
    }
  }

  async function probeServiceStatus(id, service) {
    const startedAt = process.hrtime.bigint();
    const url = resolveProbeUrl(id, service);
    let statusCode = 502;
    let error = null;

    try {
      statusCode = await requestStatusCode(url, 'HEAD');
      if (statusCode === 405 || statusCode === 501) {
        statusCode = await requestStatusCode(url, 'GET');
      }
    } catch (primaryError) {
      error = primaryError;
    }

    // Pylon relay fallback — if direct probe failed, try through the pylon
    if (error && siteConfig?.pylon) {
      const pylonResult = await probeViaPylon(url);
      if (pylonResult && pylonResult.status) {
        const responseTime = Number((process.hrtime.bigint() - startedAt) / 1000000n);
        return {
          id,
          isUp: pylonResult.status === 'healthy',
          statusCode: pylonResult.statusCode || 0,
          responseTime,
          url,
          via: 'pylon'
        };
      }
    }

    const responseTime = Number((process.hrtime.bigint() - startedAt) / 1000000n);
    if (error) {
      return {
        id,
        isUp: false,
        statusCode: 502,
        responseTime,
        error: error.message
      };
    }

    return {
      id,
      isUp: isServiceUp(statusCode),
      statusCode,
      responseTime,
      url
    };
  }

  async function mapWithConcurrency(items, limit, worker) {
    const results = new Array(items.length);
    let cursor = 0;

    async function next() {
      while (true) {
        const index = cursor++;
        if (index >= items.length) return;
        results[index] = await worker(items[index], index);
      }
    }

    const workers = Array.from({ length: Math.min(limit, items.length || 1) }, () => next());
    await Promise.all(workers);
    return results;
  }

  // ===== SERVICE CREDENTIAL ENDPOINTS =====

  // Store credentials for a service
  router.post('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
    const { serviceId } = req.params;

    // Validate serviceId to prevent path traversal in credential keys
    if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
      return errorResponse(res, 400, 'Invalid service ID');
    }

    const { apiKey, username, password } = req.body;

    if (apiKey) {
      await credentialManager.store(`service.${serviceId}.apikey`, apiKey);
    }
    if (username) {
      await credentialManager.store(`service.${serviceId}.username`, username);
    }
    if (password) {
      await credentialManager.store(`service.${serviceId}.password`, password);
    }

    success(res, { message: `Credentials stored for ${serviceId}` });
  }, 'store-service-creds'));

  // Delete credentials for a service
  router.delete('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
    const { serviceId } = req.params;

    // Validate serviceId to prevent path traversal in credential keys
    if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
      return errorResponse(res, 400, 'Invalid service ID');
    }

    await credentialManager.delete(`service.${serviceId}.apikey`);
    await credentialManager.delete(`service.${serviceId}.username`);
    await credentialManager.delete(`service.${serviceId}.password`);
    success(res, { message: `Credentials removed for ${serviceId}` });
  }, 'delete-service-creds'));

  // Check credential status for a service (what's stored)
  router.get('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
    const { serviceId } = req.params;

    // Validate serviceId to prevent path traversal in credential keys
    if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
      return errorResponse(res, 400, 'Invalid service ID');
    }
      const arrKey = await credentialManager.retrieve(`arr.${serviceId}.apikey`).catch(() => null);
      const svcKey = await credentialManager.retrieve(`service.${serviceId}.apikey`).catch(() => null);
      const username = await credentialManager.retrieve(`service.${serviceId}.username`).catch(() => null);
      success(res, {
        hasApiKey: !!(arrKey || svcKey),
        hasBasicAuth: !!username,
        username: username || null
      });
  }, 'service-creds'));

  // ===== SEEDHOST CREDENTIAL ENDPOINTS =====

  // Store seedhost credentials (shared username + per-service passwords)
  router.post('/seedhost-creds', asyncHandler(async (req, res) => {
    const { username, password, serviceId } = req.body;
    if (!username) {
      throw new ValidationError('Username required');
    }
    await credentialManager.store('seedhost.username', username);
    if (password) {
      if (serviceId) {
        await credentialManager.store(`seedhost.password.${serviceId}`, password);
      } else {
        await credentialManager.store('seedhost.password', password);
      }
    }
    success(res, { message: 'Seedhost credentials stored' });
  }, 'store-seedhost-creds'));

  // Get seedhost credential status
  router.get('/seedhost-creds', asyncHandler(async (req, res) => {
    try {
      const username = await credentialManager.retrieve('seedhost.username').catch(() => null);
      const serviceId = req.query.serviceId;
      let hasPassword = false;
      if (serviceId) {
        const svcPass = await credentialManager.retrieve(`seedhost.password.${serviceId}`).catch(() => null);
        hasPassword = !!svcPass;
      }
      // Fall back to checking shared password
      if (!hasPassword) {
        const sharedPass = await credentialManager.retrieve('seedhost.password').catch(() => null);
        hasPassword = !!sharedPass;
      }
      success(res, { hasCredentials: !!username && hasPassword, username: username || null, hasPassword });
    } catch (error) {
      success(res, { hasCredentials: false });
    }
  }, 'seedhost-creds'));

  // Delete seedhost credentials
  router.delete('/seedhost-creds', asyncHandler(async (req, res) => {
    const serviceId = req.query.serviceId;
    if (serviceId) {
      await credentialManager.delete(`seedhost.password.${serviceId}`);
      success(res, { message: `Password for ${serviceId} removed` });
    } else {
      await credentialManager.delete('seedhost.username');
      await credentialManager.delete('seedhost.password');
      success(res, { message: 'Seedhost credentials removed' });
    }
  }, 'delete-seedhost-creds'));

  // ===== SERVICE CRUD ENDPOINTS =====

  // Batched live status for dashboard cards
  const STATUS_DEADLINE = 10000; // 10s — return whatever we have by then

  router.get('/services/status', asyncHandler(async (req, res) => {
    const services = await loadServicesList();
    const serviceMap = new Map(services.filter(s => s && s.id).map(s => [s.id, s]));
    const ids = [];
    const seen = new Set();

    function addId(id) {
      if (!id || seen.has(id)) return;
      seen.add(id);
      ids.push(id);
    }

    addId('internet');
    Object.keys(siteConfig?.dnsServers || {}).forEach(addId);
    services.forEach(service => addId(service.id));

    // Collect results as they arrive; deadline returns whatever we have
    const statuses = {};
    const probeWork = mapWithConcurrency(ids, PROBE_CONCURRENCY, async (id) => {
      const result = await probeServiceStatus(id, serviceMap.get(id));
      statuses[result.id] = result;
      return result;
    });
    const deadline = new Promise((resolve) =>
      setTimeout(() => resolve(null), STATUS_DEADLINE)
    );
    await Promise.race([probeWork, deadline]);

    // Fill any IDs that didn't finish before the deadline
    const partial = ids.some((id) => !statuses[id]);
    ids.forEach((id) => {
      if (!statuses[id]) {
        statuses[id] = { id, isUp: false, statusCode: 0, responseTime: STATUS_DEADLINE, error: 'deadline' };
      }
    });

    res.set('Cache-Control', 'no-store');
    success(res, {
      checkedAt: new Date().toISOString(),
      partial,
      statuses
    });
  }, 'services-status'));

  // List all services
  // Always returns the standard envelope. The `services` field is the array
  // (paginated if ?page=N&limit=M is in the query, otherwise the full list).
  router.get('/services', asyncHandler(async (req, res) => {
    if (!await exists(SERVICES_FILE)) {
      return success(res, { services: [] });
    }
    const services = await servicesStateManager.read();
    const paginationParams = parsePaginationParams(req.query);
    const result = paginate(services, paginationParams);
    if (paginationParams) {
      success(res, { services: result.data, pagination: result.pagination });
    } else {
      success(res, { services: result.data });
    }
  }, 'services-list'));

  // Add a new service
  router.post('/services', asyncHandler(async (req, res) => {
    try {
      const { id, name, logo, category, containerId, port, ip, tailscaleOnly } = req.body;

      if (!id || !name) {
        throw new ValidationError('id and name are required');
      }

      // Validate service configuration
      try {
        validateServiceConfig({ id, name });
      } catch (validationErr) {
        return errorResponse(res, validationErr.message, 400, { errors: validationErr.errors });
      }

      await servicesStateManager.update(services => {
        // Check if service already exists
        if (services.find(s => s.id === id)) {
          throw new ConflictError(`Service "${id}" already exists`, id);
        }

        const newService = { id, name, logo: logo || `/assets/${id}.png` };
        // Persist optional metadata fields if provided
        if (category) newService.category = category;
        if (containerId) newService.containerId = containerId;
        if (port) newService.port = port;
        if (ip) newService.ip = ip;
        if (typeof tailscaleOnly === 'boolean') newService.tailscaleOnly = tailscaleOnly;
        services.push(newService);
        return services;
      });

      resyncHealthChecker?.().catch(() => {});
      success(res, { message: `Service "${name}" added to dashboard` });
    } catch (error) {
      log.error('deploy', 'Error adding service', { error: error.message });
      if (error.message.includes('already exists')) {
        errorResponse(res, safeErrorMessage(error), 409);
      } else {
        // Error handled by middleware
      }
    }
  }, 'services-update'));

  // Bulk import/replace services (for dashboard import feature)
  router.put('/services', asyncHandler(async (req, res) => {
    const services = req.body;

    if (!Array.isArray(services)) {
      throw new ValidationError('Request body must be an array of services');
    }

    for (const service of services) {
      if (!service.id || !service.name) {
        throw new ValidationError('Each service must have id and name fields');
      }
      try {
        validateServiceConfig(service);
      } catch (validationErr) {
        return errorResponse(res, `Invalid service "${service.id}": ${validationErr.message}`, 400, { errors: validationErr.errors });
      }
    }

    await servicesStateManager.write(services);
    resyncHealthChecker?.().catch(() => {});

    success(res, {
      message: `Successfully imported ${services.length} services`,
      count: services.length
    });
  }, 'services-import'));

  // Delete a service
  router.delete('/services/:id', asyncHandler(async (req, res) => {
    const { id } = req.params;

    if (!await exists(SERVICES_FILE)) {
      throw new NotFoundError('No services found');
    }

    let found = false;
    await servicesStateManager.update(services => {
      const initialLength = services.length;
      const filtered = services.filter(s => s.id !== id);
      found = filtered.length !== initialLength;
      return filtered;
    });

    if (!found) {
      return errorResponse(res, `Service "${id}" not found`, 404);
    }

    resyncHealthChecker?.().catch(() => {});
    success(res, { message: `Service "${id}" removed from dashboard` });
  }, 'services-delete'));

  // Update service configuration (subdomain, port, IP, tailscale, name, logo)
  router.post('/services/update', asyncHandler(async (req, res) => {
    const { oldSubdomain, newSubdomain, port, ip, tailscaleOnly, name, logo } = req.body;

    if (!oldSubdomain || !newSubdomain) {
      throw new ValidationError('oldSubdomain and newSubdomain are required');
    }

    if (!REGEX.SUBDOMAIN.test(oldSubdomain) || !REGEX.SUBDOMAIN.test(newSubdomain)) {
      throw new ValidationError('[DC-301] Invalid subdomain format');
    }

    if (port && !isValidPort(port)) {
      throw new ValidationError('Invalid port number (must be 1-65535)');
    }

    if (ip && ip !== 'localhost' && !validatorLib.isIP(ip)) {
      throw new ValidationError('[DC-210] Invalid IP address');
    }

    const results = { dns: null, caddy: null, services: null };

    const oldDomain = buildDomain(oldSubdomain);
    const newDomain = buildDomain(newSubdomain);

    const content = await caddy.read();

    const escapedOldDomain = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const siteBlockRegex = new RegExp(
      `${escapedOldDomain}\\s*\\{[^{}]*(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}[^{}]*)*\\}`,
      's'
    );

    const oldBlockMatch = content.match(siteBlockRegex);
    if (oldBlockMatch) {
      const proxyMatch = oldBlockMatch[0].match(/reverse_proxy\s+([^\s\n]+)/);
      const existingTarget = proxyMatch ? proxyMatch[1] : null;
      const [existingIp, existingPort] = existingTarget ? existingTarget.split(':') : ['localhost', '80'];

      const finalIp = ip || existingIp;
      const finalPort = port || existingPort;

      const newConfig = caddy.generateConfig(newSubdomain, finalIp, finalPort, {
        tailscaleOnly: tailscaleOnly || false
      });

      const caddyResult = await caddy.modify(c => c.replace(siteBlockRegex, newConfig));
      results.caddy = caddyResult.success ? 'updated' : `config saved, reload failed: ${caddyResult.error}`;
    } else {
      results.caddy = 'old config not found';
    }

    if (oldSubdomain !== newSubdomain) {
      try {
        await dns.universalDeleteRecord(oldDomain);
        await dns.universalCreateRecord(newSubdomain, ip || 'localhost');
        results.dns = 'updated';
      } catch (e) {
        results.dns = `failed: ${e.message}`;
      }
    } else {
      results.dns = 'unchanged';
    }

    if (await exists(SERVICES_FILE)) {
      await servicesStateManager.update(services => {
        const serviceIndex = services.findIndex(s => s.id === oldSubdomain || s.url?.includes(oldSubdomain));
        if (serviceIndex !== -1) {
          const existing = services[serviceIndex];
          const finalPort = port || existing.port;
          const finalIp = ip || existing.ip;

          services[serviceIndex] = {
            ...existing,
            // Keep the original ID — don't change it to the subdomain
            port: finalPort,
            ip: finalIp,
            tailscaleOnly: tailscaleOnly || false,
            url: buildServiceUrl(newSubdomain)
          };
          if (name) services[serviceIndex].name = name;
          if (logo) services[serviceIndex].logo = logo;
          // Allow category update via update endpoint too (optional body field)
          if (req.body.category !== undefined) services[serviceIndex].category = req.body.category || undefined;
          results.services = 'updated';
        } else {
          results.services = 'not found';
        }
        return services;
      });
    }

    resyncHealthChecker?.().catch(() => {});
    success(res, {
      message: `Service updated: ${oldSubdomain} -> ${newSubdomain}`,
      results
    });
  }, 'services-update'));

  return router;
};
