const express = require('express');
const fsp = require('fs').promises;
const path = require('path');
const validatorLib = require('validator');
const { REGEX, DOCKER } = require('../../src/utilities/constants');
const { isValidPort } = require('../../src/security/input-validator');
const { exists } = require('../../src/utilities/fs-helpers');
const platformPaths = require('../../platform-paths');
const { ValidationError } = require('../../src/utilities/errors');
const { logError } = require('../../src/utils/logging');
const { ok } = require('../../src/utils/responses');
/**
 * Apps deployment routes factory
 * @param {Object} deps - Explicit dependencies
 * @param {Object} deps.docker - Docker client wrapper
 * @param {Object} deps.caddy - Caddy client
 * @param {Object} deps.credentialManager - Credential manager
 * @param {Object} deps.servicesStateManager - Services state manager
 * @param {Object} deps.portLockManager - Port lock manager
 * @param {Function} deps.asyncHandler - Async route handler wrapper
 * @param {Function} deps.errorResponse - Error response helper
 * @param {Object} deps.log - Logger instance
 * @param {Object} deps.helpers - Apps helpers module
 * @returns {express.Router}
 */

module.exports = function({ docker, caddy, credentialManager, servicesStateManager, portLockManager, asyncHandler, errorResponse, log, helpers, APP_TEMPLATES, siteConfig, buildDomain, buildServiceUrl, addServiceToConfig, dns, notification, safeErrorMessage }) {
  const router = express.Router();
  
  // Ctx shim for backward compatibility with existing route code
  const ctx = {
    APP_TEMPLATES,
    siteConfig,
    buildDomain,
    buildServiceUrl,
    addServiceToConfig,
    dns,
    notification,
    safeErrorMessage
  };

  async function deployDashCAStaticSite(template, userConfig) {
    const destPath = platformPaths.caCertDir;
    try {
      log.info('deploy', 'DashCA: Starting static site deployment');
      if (!await exists(destPath)) {
        await fsp.mkdir(destPath, { recursive: true });
        log.info('deploy', 'DashCA: Created destination directory', { path: destPath });
      }

      log.info('deploy', 'DashCA: Verifying certificate files');
      const rootCertExists = await exists(`${destPath}/root.crt`);
      const intermediateCertExists = await exists(`${destPath}/intermediate.crt`);
      if (rootCertExists) log.info('deploy', 'DashCA: Root certificate found');
      else log.warn('deploy', 'DashCA: Root certificate not found', { expected: path.join(destPath, 'root.crt') });
      if (intermediateCertExists) log.info('deploy', 'DashCA: Intermediate certificate found');

      const indexPath = path.join(destPath, 'index.html');
      if (!await exists(indexPath)) {
        log.info('deploy', 'DashCA: Creating minimal landing page');
        const minimalHtml = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CA Certificate Distribution</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; background: #1a1a2e; color: #eee; }
    h1 { color: #00d9ff; }
    .download { display: inline-block; padding: 12px 24px; margin: 10px; background: #00d9ff; color: #000; text-decoration: none; border-radius: 6px; font-weight: bold; }
    .download:hover { background: #00b8d4; }
    code { background: #2a2a3e; padding: 2px 6px; border-radius: 3px; }
  </style>
</head>
<body>
  <h1>CA Certificate Installation</h1>
  <p>To trust *${ctx.siteConfig.tld} domains on your device, install the root CA certificate:</p>
  <h2>Download Certificate</h2>
  <a href="/root.crt" class="download" download>Download Certificate (.crt)</a>
  <h2>Windows Installation</h2>
  <p>Run PowerShell as Administrator:</p>
  <pre><code>irm http://ca${ctx.siteConfig.tld}/api/ca/install-script?platform=windows | iex</code></pre>
  <h2>Linux/macOS Installation</h2>
  <pre><code>curl -fsSk http://ca${ctx.siteConfig.tld}/api/ca/install-script?platform=linux | sudo bash</code></pre>
  <p><em>Note: Full DashCA interface requires manual deployment of certificate files.</em></p>
</body>
</html>`;
        await fsp.writeFile(indexPath, minimalHtml);
        log.info('deploy', 'DashCA: Created minimal landing page');
      } else {
        log.info('deploy', 'DashCA: Using existing index.html');
      }

      log.info('deploy', 'DashCA: For full features, copy certificate files to ' + destPath);
      log.info('deploy', 'DashCA: Static site deployment completed successfully');
    } catch (error) {
      log.error('deploy', 'DashCA deployment error', { error: error.message });
      throw new Error(`DashCA deployment failed: ${error.message}`);
    }
  }

  async function deployContainer(appId, userConfig, template) {
    const containerName = `${DOCKER.CONTAINER_PREFIX}${userConfig.subdomain}`;
    const processedTemplate = helpers.processTemplateVariables(template, userConfig);

    const requestedPorts = processedTemplate.docker.ports.map(portMapping => {
      const [hostPort] = portMapping.split(/[:/]/);
      return hostPort;
    });

    let lockId = null;
    try {
      log.info('deploy', 'Acquiring port locks', { ports: requestedPorts });
      lockId = await portLockManager.acquirePorts(requestedPorts);
      log.info('deploy', 'Port locks acquired', { lockId });
    } catch (lockError) {
      throw new Error(`Failed to acquire port locks: ${lockError.message}`);
    }

    try {
      // Remove stale container with same name
      try {
        const existingContainer = docker.client.getContainer(containerName);
        const info = await existingContainer.inspect();
        log.info('docker', 'Removing stale container', { containerName, status: info.State.Status });
        await existingContainer.remove({ force: true });
        await new Promise(r => setTimeout(r, 2000));
      } catch (e) {
        // Container doesn't exist — normal case
      }

      const conflicts = await helpers.checkPortConflicts(requestedPorts, containerName);
      if (conflicts.length > 0) {
        const conflictDetails = conflicts.map(c => `Port ${c.port} is in use by ${c.usedBy} (${c.app})`).join('; ');
        throw new Error(`[DC-203] Port conflict detected: ${conflictDetails}. Please choose a different port.`);
      }

      // Translate volume paths for cross-platform compatibility
      const translatedVolumes = (processedTemplate.docker.volumes || []).map(volume => {
        const [hostPath, ...rest] = volume.split(':');
        const translatedHost = platformPaths.toDockerMountPath(hostPath);
        return rest.length > 0 ? `${translatedHost}:${rest.join(':')}` : translatedHost;
      });

      const containerConfig = {
        Image: processedTemplate.docker.image,
        name: containerName,
        ExposedPorts: {},
        HostConfig: {
          PortBindings: {},
          Binds: translatedVolumes,
          RestartPolicy: { Name: 'unless-stopped' },
          LogConfig: DOCKER.LOG_CONFIG
        },
        Env: Object.entries(processedTemplate.docker.environment || {}).map(([k, v]) => `${k}=${v}`),
        Labels: {
          'sami.managed': 'true', 'sami.app': appId,
          'sami.subdomain': userConfig.subdomain,
          'sami.deployed': new Date().toISOString()
        }
      };

      processedTemplate.docker.ports.forEach(portMapping => {
        const [hostPort, containerPort, protocol = 'tcp'] = portMapping.split(/[:/]/);
        const containerPortKey = `${containerPort}/${protocol}`;
        containerConfig.ExposedPorts[containerPortKey] = {};
        containerConfig.HostConfig.PortBindings[containerPortKey] = [{ HostPort: hostPort }];
      });

      if (processedTemplate.docker.capabilities) {
        containerConfig.HostConfig.CapAdd = processedTemplate.docker.capabilities;
      }

      // Resource limits (CPU and memory)
      if (userConfig.resources) {
        if (userConfig.resources.memory) {
          const memBytes = Math.round(userConfig.resources.memory * 1024 * 1024); // MB to bytes
          containerConfig.HostConfig.Memory = memBytes;
          containerConfig.HostConfig.MemoryReservation = Math.round(memBytes * 0.5); // soft limit = 50%
        }
        if (userConfig.resources.cpus) {
          containerConfig.HostConfig.NanoCpus = Math.round(userConfig.resources.cpus * 1e9);
        }
      }

      try {
        log.info('docker', 'Pulling image', { image: processedTemplate.docker.image });
        await docker.pull(processedTemplate.docker.image);
        log.info('docker', 'Image pulled successfully', { image: processedTemplate.docker.image });
      } catch (e) {
        log.warn('docker', 'Image pull failed, checking if local image exists', { image: processedTemplate.docker.image, error: e.message });
        try {
          const images = await docker.client.listImages({ filters: { reference: [processedTemplate.docker.image] } });
          if (images.length === 0) throw new Error(`[DC-201] Image ${processedTemplate.docker.image} not found locally and pull failed: ${e.message}`);
          log.info('docker', 'Using existing local image', { image: processedTemplate.docker.image });
        } catch (listError) {
          throw new Error(`[DC-201] Failed to pull or find image ${processedTemplate.docker.image}: ${e.message}`);
        }
      }

      let container;
      try {
        container = await docker.client.createContainer(containerConfig);
        await container.start();
      } catch (createErr) {
        // If create fails with "no such image", wrap with user-friendly message
        const errMsg = createErr?.message || String(createErr);
        if (errMsg.includes('No such image') || errMsg.includes('no such image')) {
          throw new Error(`[DC-201] Image pull succeeded but container creation failed — image may be corrupted: ${processedTemplate.docker.image}. ${errMsg}`);
        }
        throw createErr;
      }

      // Prune dangling images to prevent disk bloat
      try {
        const pruneResult = await docker.client.pruneImages({ filters: { dangling: { true: true } } });
        if (pruneResult.SpaceReclaimed > 0) {
          log.info('docker', 'Pruned dangling images after deploy', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
        }
      } catch (pruneErr) {
        log.debug('docker', 'Image prune after deploy failed', { error: pruneErr.message });
      }

      await portLockManager.releasePorts(lockId);
      log.info('deploy', 'Port locks released', { lockId });
      return container.id;
    } catch (deployError) {
      if (lockId) {
        try {
          await portLockManager.releasePorts(lockId);
          log.info('deploy', 'Port locks released after error', { lockId });
        } catch (releaseError) {
          log.error('deploy', 'Failed to release port locks', { lockId, error: releaseError.message });
        }
      }
      throw deployError;
    }
  }

  // Check for existing container before deployment
  router.post('/check-existing', asyncHandler(async (req, res) => {
    const { appId } = req.body;
    const template = ctx.APP_TEMPLATES[appId];
    if (!template) throw new ValidationError('Invalid app template');
    const existingContainer = await helpers.findExistingContainerByImage(template);
    if (existingContainer) {
      ok(res, { exists: true, container: existingContainer, message: `Found existing ${template.name} container: ${existingContainer.name}` });
    } else {
      ok(res, { exists: false, message: `No existing ${template.name} container found` });
    }
  }, 'check-existing'));

  // Deploy new app
  router.post('/deploy', asyncHandler(async (req, res) => {
    const { appId, config } = req.body;
    if (!appId || typeof appId !== 'string') {
      throw new ValidationError('appId is required');
    }
    if (!config || typeof config !== 'object') {
      throw new ValidationError('config object is required');
    }
    if (!config.subdomain || typeof config.subdomain !== 'string') {
      throw new ValidationError('config.subdomain is required');
    }
    try {
      log.info('deploy', 'Deploying app', { appId, subdomain: config.subdomain });
      const template = ctx.APP_TEMPLATES[appId];
      if (!template) {
        await logError('app-deploy', new Error('Invalid app template'), { appId, config });
        throw new ValidationError('Invalid app template');
      }

      if (config.subdomain) {
        if (!REGEX.SUBDOMAIN.test(config.subdomain)) {
          throw new ValidationError('[DC-301] Invalid subdomain format');
        }
        // Block reserved path names in subdirectory mode
        if (ctx.siteConfig.routingMode === 'subdirectory' && helpers.RESERVED_SUBPATHS.includes(config.subdomain)) {
          return errorResponse(res, 400, `[DC-301] "${config.subdomain}" is a reserved path and cannot be used in subdirectory mode`);
        }
      }
      if (config.port && !isValidPort(config.port)) {
        throw new ValidationError('Invalid port number (must be 1-65535)');
      }

      if (!template.isStaticSite) {
        const allowedHostnames = ['localhost', 'host.docker.internal'];
        if (config.ip && !validatorLib.isIP(config.ip) && !allowedHostnames.includes(config.ip)) {
          return errorResponse(res, 400, '[DC-210] Invalid IP address. Use a valid IP (e.g., 192.168.x.x) or "localhost".');
        }
        if (!config.ip) config.ip = ctx.siteConfig.dnsServerIp || 'localhost';
      } else {
        config.createDns = false;
        config.ip = ctx.siteConfig.dnsServerIp || 'localhost';
      }

      let containerId;
      let usedExisting = false;
      
      // Process template variables for manifest (only needed for Docker containers)
      const processedTemplate = template.isStaticSite ? null : helpers.processTemplateVariables(template, config);

      if (template.isStaticSite) {
        log.info('deploy', 'Deploying static site', { appId });
        if (appId === 'dashca') {
          await deployDashCAStaticSite(template, config);
          containerId = null;
          log.info('deploy', 'Static site deployed', { appId });
        } else {
          throw new Error(`Unknown static site type: ${appId}`);
        }
      } else if (config.useExisting && config.existingContainerId) {
        containerId = config.existingContainerId;
        usedExisting = true;
        log.info('deploy', 'Using existing container', { containerId });
        if (config.existingPort && !config.port) config.port = config.existingPort;
      } else {
        containerId = await deployContainer(appId, config, template);
        log.info('deploy', 'Container deployed', { containerId });
        await helpers.waitForHealthCheck(containerId, template.healthCheck, config.port || template.defaultPort, 30);
        log.info('deploy', 'Container is healthy', { containerId });
      }

      const isSubdirectoryMode = ctx.siteConfig.routingMode === 'subdirectory' && ctx.siteConfig.domain;

      // DNS record creation (skip in subdirectory mode — only one domain needed)
      let dnsWarning = null;
      if (config.createDns && !isSubdirectoryMode) {
        try {
          await ctx.dns.universalCreateRecord(config.subdomain, config.ip);
          log.info('deploy', 'DNS record created', { domain: ctx.buildDomain(config.subdomain), ip: config.ip });
        } catch (dnsError) {
          await logError('app-deploy-dns', dnsError, { appId, subdomain: config.subdomain, ip: config.ip });
          dnsWarning = `DNS creation failed: ${dnsError.message}. You may need to create the DNS record manually.`;
          log.warn('deploy', 'DNS creation failed during deploy', { error: dnsError.message });
        }
      }

      // Caddy config generation
      const caddyOptions = {
        tailscaleOnly: config.tailscaleOnly || false,
        allowedIPs: config.allowedIPs || [],
        subpathSupport: template.subpathSupport || 'strip',
      };
      let caddyConfig;
      if (template.isStaticSite) {
        const sitePath = platformPaths.sitePath(config.subdomain);
        if (appId === 'dashca') {
          caddyOptions.httpAccess = true;
          caddyOptions.apiProxy = 'host.docker.internal:3001';
        }
        caddyConfig = helpers.generateStaticSiteConfig(config.subdomain, sitePath, caddyOptions);
      } else {
        caddyConfig = caddy.generateConfig(config.subdomain, config.ip, config.port || template.defaultPort, caddyOptions);
      }

      // Write Caddy config (subdirectory: inject into main block; subdomain: append as new block)
      if (isSubdirectoryMode && !template.isStaticSite) {
        await helpers.ensureMainDomainBlock();
        await helpers.addSubpathConfig(config.subdomain, caddyConfig);
      } else {
        await helpers.addCaddyConfig(config.subdomain, caddyConfig);
      }
      log.info('deploy', 'Caddy config added', { domain: ctx.buildDomain(config.subdomain), routingMode: ctx.siteConfig.routingMode, tailscaleOnly: config.tailscaleOnly || false });

      // Build service URL based on routing mode
      const serviceUrl = ctx.buildServiceUrl(config.subdomain);

      // Build deployment manifest — the full recipe to recreate this container
      const deploymentManifest = {
        templateId: appId,
        config: {
          subdomain: config.subdomain,
          port: config.port || template.defaultPort,
          ip: config.ip,
          mediaPath: config.mediaPath || undefined,
          createDns: config.createDns || false,
          tailscaleOnly: config.tailscaleOnly || false,
          allowedIPs: config.allowedIPs || [],
          customVolumes: config.customVolumes || undefined,
          useExisting: false
        },
        container: template.isStaticSite ? null : {
          image: processedTemplate.docker.image,
          ports: processedTemplate.docker.ports,
          volumes: processedTemplate.docker.volumes || [],
          environment: (() => {
            // Strip sensitive values from stored env (claim tokens, secrets)
            const env = { ...processedTemplate.docker.environment };
            for (const key of Object.keys(env)) {
              if (/claim|secret|password|token|key/i.test(key) && env[key]) {
                env[key] = '';  // Clear sensitive values — user re-enters on restore
              }
            }
            return env;
          })(),
          capabilities: processedTemplate.docker.capabilities || undefined
        },
        caddy: {
          tailscaleOnly: config.tailscaleOnly || false,
          allowedIPs: config.allowedIPs || [],
          subpathSupport: template.subpathSupport || 'strip',
          routingMode: ctx.siteConfig.routingMode
        }
      };

      await ctx.addServiceToConfig({
        id: config.subdomain, name: template.name,
        logo: template.logo || `/assets/${appId}.png`,
        url: serviceUrl,
        containerId, appTemplate: appId,
        tailscaleOnly: config.tailscaleOnly || false,
        routingMode: ctx.siteConfig.routingMode,
        deployedAt: new Date().toISOString(),
        deploymentManifest
      });
      log.info('deploy', 'Service added to dashboard', { subdomain: config.subdomain });

      const response = {
        success: true, containerId, usedExisting,
        url: serviceUrl,
        message: usedExisting ? `${template.name} configured using existing container!` : `${template.name} deployed successfully!`,
        setupInstructions: template.setupInstructions || []
      };
      if (dnsWarning) response.warning = dnsWarning;

      const notificationMessage = usedExisting
        ? `**${template.name}** configured using existing container.\nURL: ${serviceUrl}`
        : `**${template.name}** has been deployed successfully.\nURL: ${serviceUrl}`;
      ctx.notification.send('deploymentSuccess', usedExisting ? 'Configuration Complete' : 'Deployment Successful', notificationMessage, 'success');

      res.json(response);
    } catch (error) {
      try { await logError('app-deploy', error, { appId, config }); } catch (_) { /* logError failure should not mask original error */ }
      const msg = error?.message || String(error || 'Unknown error');
      log.error('deploy', 'Deployment failed', { appId, error: msg });
      const template = ctx.APP_TEMPLATES[appId];
      try { ctx.notification.send('deploymentFailed', 'Deployment Failed', `Failed to deploy **${template?.name || appId}**.\nError: ${msg}`, 'error'); } catch (_) {}
      errorResponse(res, 500, ctx.safeErrorMessage(error));
    }
  }, 'apps-deploy'));

  return router;
};
