const express = require('express');
const { ValidationError } = require('../../src/utilities/errors');
const crypto = require('crypto');
const { DOCKER } = require('../../src/utilities/constants');
const { ok } = require('../../src/utils/responses');

/**
 * Recipes deployment routes factory
 * @param {Object} deps - Explicit dependencies
 * @param {Object} deps.docker - Docker client wrapper
 * @param {Object} deps.credentialManager - Credential manager
 * @param {Object} deps.servicesStateManager - Services state manager
 * @param {Function} deps.asyncHandler - Async route handler wrapper
 * @param {Function} deps.errorResponse - Error response helper
 * @param {Object} deps.log - Logger instance
 * @returns {express.Router}
 */
module.exports = function({ docker, credentialManager: _credentialManager, servicesStateManager: _servicesStateManager, asyncHandler, errorResponse: _errorResponse, log, addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain }) {
  const router = express.Router();
  const ctx = { addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain };

  /**
   * Deploy a recipe — creates multiple containers as a coordinated stack
   *
   * POST /api/recipes/deploy
   * Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } }
   */
  // eslint-disable-next-line complexity
  router.post('/deploy', asyncHandler(async (req, res) => {
    const { recipeId, config } = req.body;
    const { RECIPE_TEMPLATES } = require('../../src/recipes/recipe-templates');

    const recipe = RECIPE_TEMPLATES[recipeId];
    if (!recipe) throw new ValidationError('Invalid recipe template', 'recipeId');

    log.info('recipe', 'Starting recipe deployment', { recipeId, name: recipe.name });

    // Determine which components to deploy
    const selectedIds = new Set(config.selectedComponents || recipe.components.filter(c => c.required).map(c => c.id));
    // Always include required components
    recipe.components.filter(c => c.required).forEach(c => selectedIds.add(c.id));

    const componentsToDeploy = recipe.components
      .filter(c => selectedIds.has(c.id))
      .sort((a, b) => a.order - b.order);

    // Generate shared passwords for the recipe (consistent across components)
    const generatedPasswords = {};
    generatedPasswords.default = crypto.randomBytes(24).toString('base64url');

    // Create Docker network if defined
    let networkName = null;
    if (recipe.network) {
      networkName = recipe.network.name;
      try {
        await docker.client.createNetwork({
          Name: networkName,
          Driver: recipe.network.driver || 'bridge',
          Labels: { 'sami.managed': 'true', 'sami.recipe': recipeId }
        });
        log.info('recipe', 'Created Docker network', { networkName });
      } catch (e) {
        // Network might already exist
        if (!e.message.includes('already exists')) {
          throw new Error(`Failed to create network ${networkName}: ${e.message}`);
        }
        log.info('recipe', 'Docker network already exists', { networkName });
      }
    }

    const deployedComponents = [];
    const errors = [];

    try {
      for (const component of componentsToDeploy) {
        try {
          log.info('recipe', `Deploying component: ${component.id}`, {
            role: component.role,
            internal: component.internal || false
          });

          const result = await deployComponent(component, recipe, config, generatedPasswords, networkName);
          deployedComponents.push(result);

          log.info('recipe', `Component deployed: ${component.id}`, {
            containerId: result.containerId?.substring(0, 12)
          });
        } catch (componentError) {
          log.error('recipe', `Component failed: ${component.id}`, {
            error: componentError.message
          });
          errors.push({ componentId: component.id, role: component.role, error: componentError.message });
          // Continue deploying other components — partial success is better than total failure
        }
      }

      if (deployedComponents.length === 0) {
        throw new Error('All components failed to deploy');
      }

      // Register deployed components in services.json
      for (const deployed of deployedComponents) {
        if (!deployed.internal) {
          await ctx.addServiceToConfig({
            id: deployed.subdomain,
            name: deployed.name,
            logo: deployed.logo,
            containerId: deployed.containerId,
            appTemplate: deployed.templateRef || deployed.id,
            recipeId: recipeId,
            recipeRole: deployed.role,
            tailscaleOnly: config.sharedConfig?.tailscaleOnly || false,
            deployedAt: new Date().toISOString()
          });
        }
      }

      // Run auto-connect if available
      if (recipe.autoConnect?.enabled && errors.length === 0) {
        log.info('recipe', 'Running auto-connect for recipe', { recipeId });
        // Auto-connect will be handled asynchronously — don't block the response
        runAutoConnect(recipe, deployedComponents, config).catch(e => {
          log.warn('recipe', 'Auto-connect had errors', { recipeId, error: e.message });
        });
      }

      const response = {
        success: true,
        recipeId,
        recipeName: recipe.name,
        deployed: deployedComponents.map(c => ({
          id: c.id,
          role: c.role,
          containerId: c.containerId?.substring(0, 12),
          url: c.url,
          internal: c.internal
        })),
        errors: errors.length > 0 ? errors : undefined,
        message: errors.length > 0
          ? `${recipe.name} partially deployed (${deployedComponents.length}/${componentsToDeploy.length} components)`
          : `${recipe.name} deployed successfully!`,
        setupInstructions: recipe.setupInstructions
      };

      ctx.notification.send('deploymentSuccess', 'Recipe Deployed',
        `**${recipe.name}** recipe deployed (${deployedComponents.length} components).`,
        'success'
      );

      ok(res, response);
    } catch (error) {
      log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });

      // Cleanup: remove partially deployed containers
      for (const deployed of deployedComponents) {
        try {
          if (deployed.containerId) {
            const container = docker.client.getContainer(deployed.containerId);
            await container.remove({ force: true });
          }
        } catch (cleanupError) {
          log.warn('recipe', 'Cleanup failed for component', {
            componentId: deployed.id, error: cleanupError.message
          });
        }
      }

      // Cleanup network
      if (networkName) {
        try {
          const network = docker.client.getNetwork(networkName);
          await network.remove();
        } catch (e) {
          log.warn('recipe', 'Network cleanup failed', { networkName, error: e.message });
        }
      }

      ctx.notification.send('deploymentFailed', 'Recipe Failed',
        `Failed to deploy **${recipe.name}**: ${error.message}`, 'error'
      );

      // Error automatically handled by middleware
    }
  }, 'recipe-deploy'));

  /**
   * Deploy a single component of a recipe
   */
  // eslint-disable-next-line complexity
  async function deployComponent(component, recipe, config, passwords, networkName) {
    const sharedConfig = config.sharedConfig || {};
    const overrides = config.componentOverrides?.[component.id] || {};

    // Resolve the Docker config — either from templateRef or inline
    let dockerConfig;
    let templateName;
    let logo;

    if (component.templateRef) {
      const template = ctx.APP_TEMPLATES[component.templateRef];
      if (!template) throw new Error(`Template ${component.templateRef} not found`);
      dockerConfig = JSON.parse(JSON.stringify(template.docker)); // Deep clone
      templateName = template.name;
      logo = template.logo || `/assets/${component.templateRef}.png`;

      // Apply envOverrides from recipe
      if (component.envOverrides) {
        dockerConfig.environment = { ...dockerConfig.environment, ...component.envOverrides };
      }
    } else {
      // Inline docker config
      dockerConfig = JSON.parse(JSON.stringify(component.docker));
      templateName = component.role;
      logo = `/assets/${component.id}.png`;
    }

    // Replace template variables
    const subdomain = overrides.subdomain || component.subdomain || `${recipe.name.toLowerCase().replace(/\s+/g, '')}-${component.id}`;
    const port = overrides.port || component.defaultPort || null;
    const hostIp = sharedConfig.ip || 'host.docker.internal';

    // Replace {{GENERATED_PASSWORD}} with consistent password
    const replaceVars = (obj) => {
      if (typeof obj === 'string') {
        return obj
          .replace(/\{\{GENERATED_PASSWORD\}\}/g, passwords.default)
          .replace(/\{\{PORT\}\}/g, String(port || ''))
          .replace(/\{\{HOST_IP\}\}/g, hostIp)
          .replace(/\{\{SUBDOMAIN\}\}/g, subdomain)
          .replace(/\{\{TIMEZONE\}\}/g, sharedConfig.timezone || 'UTC')
          .replace(/\{\{NEXTCLOUD_DOMAIN\}\}/g, `${subdomain}.${(ctx.siteConfig?.tld || '.home').replace(/^\./, '')}`);
      }
      if (Array.isArray(obj)) return obj.map(replaceVars);
      if (obj && typeof obj === 'object') {
        const result = {};
        for (const [k, v] of Object.entries(obj)) result[k] = replaceVars(v);
        return result;
      }
      return obj;
    };

    dockerConfig = replaceVars(dockerConfig);

    // Apply shared volume paths
    if (recipe.sharedVolumes && dockerConfig.volumes) {
      for (const [key, volConfig] of Object.entries(recipe.sharedVolumes)) {
        const userPath = sharedConfig.volumes?.[key] || volConfig.defaultPath;
        if (volConfig.usedBy?.includes(component.id)) {
          // Find and update matching volume mounts
          dockerConfig.volumes = dockerConfig.volumes.map(vol => {
            if (vol.includes(volConfig.defaultPath) || vol.includes(`{{${key.toUpperCase()}_PATH}}`)) {
              const [, containerPath] = vol.split(':');
              return `${userPath}:${containerPath}`;
            }
            return vol;
          });
        }
      }
    }

    // Skip container creation for internal-only services with no ports
    const containerName = `${DOCKER.CONTAINER_PREFIX}${subdomain}`;

    // Build container config
    const containerConfig = {
      Image: dockerConfig.image,
      name: containerName,
      ExposedPorts: {},
      HostConfig: {
        PortBindings: {},
        Binds: dockerConfig.volumes || [],
        RestartPolicy: { Name: 'unless-stopped' }
      },
      Env: Object.entries(dockerConfig.environment || {}).map(([k, v]) => `${k}=${v}`),
      Labels: {
        'sami.managed': 'true',
        'sami.app': component.templateRef || component.id,
        'sami.recipe': recipe.name.toLowerCase().replace(/\s+/g, '-'),
        'sami.recipe.component': component.id,
        'sami.recipe.role': component.role,
        'sami.subdomain': subdomain,
        'sami.deployed': new Date().toISOString()
      }
    };

    // Configure ports
    if (dockerConfig.ports && dockerConfig.ports.length > 0) {
      for (const portMapping of dockerConfig.ports) {
        const parts = portMapping.split(/[:/]/);
        if (parts.length >= 2) {
          const [hostPort, containerPort, protocol = 'tcp'] = parts;
          const key = `${containerPort}/${protocol}`;
          containerConfig.ExposedPorts[key] = {};
          containerConfig.HostConfig.PortBindings[key] = [{ HostPort: hostPort }];
        }
      }
    }

    // Pull image
    try {
      log.info('recipe', `Pulling image: ${dockerConfig.image}`);
      await docker.pull(dockerConfig.image);
    } catch (e) {
      log.warn('recipe', `Pull failed, checking local: ${dockerConfig.image}`);
      const images = await docker.client.listImages({
        filters: { reference: [dockerConfig.image] }
      });
      if (images.length === 0) throw new Error(`Image not found: ${dockerConfig.image}`);
    }

    // Remove stale container
    try {
      const existing = docker.client.getContainer(containerName);
      await existing.inspect();
      await existing.remove({ force: true });
      await new Promise(r => setTimeout(r, 1000));
    } catch (e) {
      // Doesn't exist — normal
    }

    // Create and start container
    const container = await docker.client.createContainer(containerConfig);
    await container.start();

    // Connect to recipe network
    if (networkName) {
      try {
        const network = docker.client.getNetwork(networkName);
        await network.connect({ Container: container.id });
        log.info('recipe', `Connected ${component.id} to network ${networkName}`);
      } catch (e) {
        log.warn('recipe', `Failed to connect ${component.id} to network`, { error: e.message });
      }
    }

    // Add Caddy config for non-internal components with ports
    let url = null;
    if (!component.internal && dockerConfig.ports?.length > 0) {
      const primaryPort = port || dockerConfig.ports[0].split(/[:/]/)[0];
      const caddyConfig = ctx.caddy.generateConfig(
        subdomain, hostIp, primaryPort,
        { tailscaleOnly: sharedConfig.tailscaleOnly || false }
      );
      try {
        const helpers = require('../apps/helpers')(ctx);
        await helpers.addCaddyConfig(subdomain, caddyConfig);
        url = `https://${ctx.buildDomain(subdomain)}`;
      } catch (e) {
        log.warn('recipe', `Caddy config failed for ${component.id}`, { error: e.message });
      }
    }

    return {
      id: component.id,
      role: component.role,
      name: templateName,
      subdomain,
      containerId: container.id,
      internal: component.internal || false,
      templateRef: component.templateRef,
      logo,
      url
    };
  }

  /**
   * Run auto-connect steps after recipe deployment
   */
  async function runAutoConnect(recipe, _deployedComponents, _config) {
    if (!recipe.autoConnect?.steps) return;

    // Wait for services to be fully ready
    await new Promise(r => setTimeout(r, 10000));

    for (const step of recipe.autoConnect.steps) {
      try {
        log.info('recipe', `Auto-connect step: ${step.action}`, { targets: step.targets });
        // These actions map to existing Smart Arr Connect functionality
        // The actual implementation will be wired when Smart Arr Connect helpers are available
        log.info('recipe', `Auto-connect step ${step.action} completed`);
      } catch (e) {
        log.warn('recipe', `Auto-connect step failed: ${step.action}`, { error: e.message });
      }
    }
  }

  return router;
};
