const fs = require('fs');
const fsp = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const { REGEX, DOCKER } = require('../../src/utilities/constants');
const { exists } = require('../../src/utilities/fs-helpers');
const platformPaths = require('../../platform-paths');

/**
 * Apps helpers 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 {Function} deps.fetchT - Timeout-wrapped fetch
 * @param {Object} deps.log - Logger instance
 * @returns {Object} Helper functions
 */
module.exports = function({ docker, caddy, credentialManager, servicesStateManager, fetchT, log, ctx }) {

  async function checkPortConflicts(ports, excludeContainerName = null) {
    const conflicts = [];
    try {
      const containers = await docker.client.listContainers({ all: true });
      for (const container of containers) {
        if (excludeContainerName && container.Names.some(n => n === `/${excludeContainerName}`)) continue;
        if (container.State !== 'running') continue;
        for (const portInfo of (container.Ports || [])) {
          if (portInfo.PublicPort) {
            const publicPort = portInfo.PublicPort.toString();
            if (ports.includes(publicPort)) {
              const containerName = container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12);
              const appLabel = container.Labels?.['sami.app'] || 'unknown';
              conflicts.push({ port: publicPort, usedBy: containerName, app: appLabel, containerId: container.Id.substring(0, 12) });
            }
          }
        }
      }
    } catch (e) {
      log.warn('docker', 'Could not check port conflicts', { error: e.message });
    }
    return conflicts;
  }

  async function findExistingContainerByImage(template) {
    try {
      const containers = await docker.client.listContainers({ all: false });
      const templateImage = template.docker.image.split(':')[0];
      for (const container of containers) {
        const containerImage = container.Image.split(':')[0];
        if (containerImage === templateImage || containerImage.endsWith('/' + templateImage)) {
          const ports = container.Ports.filter(p => p.PublicPort).map(p => ({
            hostPort: p.PublicPort, containerPort: p.PrivatePort, protocol: p.Type
          }));
          return {
            id: container.Id, shortId: container.Id.slice(0, 12),
            name: container.Names[0]?.replace(/^\//, '') || 'unknown',
            image: container.Image, status: container.Status, state: container.State,
            ports, primaryPort: ports.length > 0 ? ports[0].hostPort : null,
            labels: container.Labels || {}
          };
        }
      }
      return null;
    } catch (e) {
      log.warn('docker', 'Could not check for existing containers', { error: e.message });
      return null;
    }
  }

  // Convert host path to Docker-compatible mount format (platform-aware)
  const toDockerDesktopPath = platformPaths.toDockerMountPath;

  function processTemplateVariables(template, config) {
    const processed = JSON.parse(JSON.stringify(template));
    const mediaPathInput = config.mediaPath || template.mediaMount?.defaultPath || '/media';
    const mediaPaths = mediaPathInput.split(',').map(p => p.trim()).filter(p => p).map(p => toDockerDesktopPath(p));

    const replacements = {
      '{{HOST_IP}}': config.ip,
      '{{SUBDOMAIN}}': config.subdomain,
      '{{PORT}}': config.port || template.defaultPort,
      '{{MEDIA_PATH}}': mediaPaths[0] || '/media',
      '{{TIMEZONE}}': ctx.siteConfig.timezone || 'UTC',
      '{{GENERATED_SECRET}}': crypto.randomBytes(32).toString('hex')
    };

    function replaceInObject(obj) {
      for (const key in obj) {
        if (typeof obj[key] === 'string') {
          Object.entries(replacements).forEach(([placeholder, value]) => {
            obj[key] = obj[key].replace(new RegExp(placeholder, 'g'), value);
          });
        } else if (typeof obj[key] === 'object' && obj[key] !== null) {
          replaceInObject(obj[key]);
        }
      }
    }

    replaceInObject(processed);

    // Handle multiple media paths
    if (mediaPaths.length > 1 && processed.docker?.volumes) {
      const containerPath = template.mediaMount?.containerPath || '/media';
      const newVolumes = [];
      for (const vol of processed.docker.volumes) {
        if (vol.includes(mediaPaths[0]) && vol.includes(containerPath)) {
          for (const p of mediaPaths) {
            const folderName = p.split(/[/\\]/).filter(p => p).pop() || 'media';
            newVolumes.push(`${p}:${containerPath}/${folderName}`);
          }
        } else {
          newVolumes.push(vol);
        }
      }
      processed.docker.volumes = newVolumes;
    }

    // Handle Plex claim token
    if (config.plexClaimToken && processed.docker?.environment?.PLEX_CLAIM !== undefined) {
      processed.docker.environment.PLEX_CLAIM = config.plexClaimToken;
    }

    // Inject URL base env var for subdirectory routing mode
    if (ctx.siteConfig.routingMode === 'subdirectory' && template.urlBaseEnv) {
      processed.docker.environment = processed.docker.environment || {};
      const basePath = `/${config.subdomain}`;
      // Some apps need the full URL, not just the path
      if (['GF_SERVER_ROOT_URL', 'GITEA__server__ROOT_URL'].includes(template.urlBaseEnv)) {
        processed.docker.environment[template.urlBaseEnv] = ctx.buildServiceUrl(config.subdomain) + '/';
      } else {
        processed.docker.environment[template.urlBaseEnv] = basePath;
      }
    }

    // Apply custom volume overrides
    if (config.customVolumes?.length && processed.docker?.volumes) {
      processed.docker.volumes = processed.docker.volumes.map(vol => {
        const parts = vol.split(':');
        const containerPath = parts.slice(1).join(':');
        const override = config.customVolumes.find(cv => cv.containerPath === containerPath);
        if (override && override.hostPath) {
          // Validate host path is under allowed roots (docker data dir or media paths)
          const normalizedHost = path.resolve(override.hostPath);
          const allowedRoots = [path.resolve(platformPaths.dockerData)];
          if (config.mediaPath) {
            config.mediaPath.split(',').map(p => p.trim()).filter(Boolean).forEach(p => allowedRoots.push(path.resolve(p)));
          }
          const isAllowed = allowedRoots.some(root =>
            normalizedHost === root || normalizedHost.startsWith(root + path.sep)
          );
          if (!isAllowed) {
            log.warn('deploy', 'Custom volume host path rejected', { hostPath: override.hostPath, allowed: allowedRoots });
            return vol; // Keep original volume, don't apply unsafe override
          }
          return `${toDockerDesktopPath(override.hostPath)}:${containerPath}`;
        }
        return vol;
      });
    }

    return processed;
  }

  function generateStaticSiteConfig(subdomain, sitePath, options = {}) {
    const { tailscaleOnly = false, httpAccess = false, apiProxy = null } = options;
    const domain = ctx.buildDomain(subdomain);

    // Shared block content used by both HTTPS and HTTP blocks
    function siteBlockContent() {
      let c = '';
      c += `  root * ${sitePath}\n\n`;

      if (tailscaleOnly) {
        c += `  @blocked not remote_ip 100.64.0.0/10\n`;
        c += `  respond @blocked "Access denied. Tailscale connection required." 403\n\n`;
      }

      if (apiProxy) {
        c += `  handle /api/* {\n`;
        c += `    reverse_proxy ${apiProxy}\n`;
        c += `  }\n\n`;
      }

      c += `  @crt path *.crt\n`;
      c += `  handle @crt {\n`;
      c += `    header Content-Type application/x-x509-ca-cert\n`;
      c += `    header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
      c += `    header Cache-Control "public, max-age=86400"\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  @der path *.der\n`;
      c += `  handle @der {\n`;
      c += `    header Content-Type application/x-x509-ca-cert\n`;
      c += `    header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
      c += `    header Cache-Control "public, max-age=86400"\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  @mobileconfig path *.mobileconfig\n`;
      c += `  handle @mobileconfig {\n`;
      c += `    header Content-Type application/x-apple-aspen-config\n`;
      c += `    header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
      c += `    header Cache-Control "public, max-age=86400"\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  @ps1 path *.ps1\n`;
      c += `  handle @ps1 {\n`;
      c += `    header Content-Type text/plain\n`;
      c += `    header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  @sh path *.sh\n`;
      c += `  handle @sh {\n`;
      c += `    header Content-Type text/x-shellscript\n`;
      c += `    header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  # Static site with SPA fallback\n`;
      c += `  handle {\n`;
      c += `    @notFile not file {path}\n`;
      c += `    rewrite @notFile /index.html\n`;
      c += `    file_server\n`;
      c += `  }\n\n`;
      c += `  # No cache for HTML\n`;
      c += `  @htmlfiles {\n`;
      c += `    path *.html\n`;
      c += `    path /\n`;
      c += `  }\n`;
      c += `  header @htmlfiles Cache-Control "no-store"\n`;
      return c;
    }

    // HTTPS block
    let config = `${domain} {\n`;
    config += `  tls internal\n\n`;
    config += siteBlockContent();
    config += `}`;

    // HTTP companion block for devices that haven't trusted the CA yet
    if (httpAccess) {
      config += `\n\n# HTTP access for first-time certificate installation\n`;
      config += `http://${domain} {\n`;
      config += siteBlockContent();
      config += `}`;
    }

    return config;
  }

  async function waitForHealthCheck(containerId, healthPath, port, maxAttempts = 20) {
    const delay = 2000;
    let httpCheckFailed = 0;

    for (let i = 0; i < maxAttempts; i++) {
      try {
        const container = docker.client.getContainer(containerId);
        const info = await container.inspect();
        if (info.State.Running) {
          if (info.State.Health) {
            if (info.State.Health.Status === 'healthy') {
              log.info('docker', 'Container is healthy (Docker health check)', { containerId });
              return true;
            }
          } else if (healthPath && port && httpCheckFailed < 5) {
            try {
              const response = await fetchT(`http://localhost:${port}${healthPath}`, {
                signal: AbortSignal.timeout(3000), redirect: 'manual'
              });
              if (response.ok || (response.status >= 300 && response.status < 400)) {
                log.info('docker', 'Health check passed', { containerId, status: response.status });
                return true;
              }
            } catch (e) {
              httpCheckFailed++;
              log.debug('docker', 'HTTP health check failed', { attempt: httpCheckFailed, error: e.message });
            }
          } else {
            if (i >= 5) {
              log.info('docker', 'Container is running', { containerId, waitedSeconds: i * delay / 1000 });
              return true;
            }
          }
        }
      } catch (e) {
        log.debug('docker', 'Health check attempt failed', { attempt: i + 1, error: e.message });
      }
      if (i < maxAttempts - 1) {
        log.debug('docker', 'Waiting for container to be healthy', { attempt: i + 1, maxAttempts });
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
    throw new Error(`[DC-202] Container failed to become healthy after ${maxAttempts} attempts (${maxAttempts * delay / 1000}s)`);
  }

  async function addCaddyConfig(subdomain, config) {
    const domain = ctx.buildDomain(subdomain);
    const existing = await caddy.read();
    if (existing.includes(`${domain} {`)) {
      log.info('caddy', 'Caddy config already exists, skipping add', { domain });
      await caddy.reload(existing);
      return;
    }
    const result = await caddy.modify(c => c + `\n${config}\n`);
    if (!result.success) throw new Error(`[DC-303] Failed to add Caddy config for ${domain}: ${result.error}`);
    await caddy.verifySite(domain);
  }

  // Reserved paths that cannot be used as subpath names in subdirectory mode
  const RESERVED_SUBPATHS = ['api', 'probe', 'assets', 'health', 'dist', 'js', 'css', 'fonts', 'favicon.ico'];

  /** Ensure the main domain block with route markers exists in the Caddyfile.
   *  Called before the first subdirectory app is deployed. */
  async function ensureMainDomainBlock() {
    if (ctx.siteConfig.routingMode !== 'subdirectory' || !ctx.siteConfig.domain) return;

    const content = await caddy.read();
    const domain = ctx.siteConfig.domain;
    const ROUTE_MARKER = '# === DashCaddy App Routes ===';

    // Already has markers — nothing to do
    if (content.includes(ROUTE_MARKER)) return;

    // Domain block exists but lacks markers — inject them
    if (content.includes(`${domain} {`)) {
      const result = await caddy.modify(c => {
        // Insert markers before the final catch-all handle block inside the domain block
        const domainStart = c.indexOf(`${domain} {`);
        // Find standalone "handle {" (catch-all SPA fallback) — match tabs or spaces
        const searchFrom = domainStart + domain.length;
        const handleMatch = c.slice(searchFrom).match(/^([ \t]+)handle\s*\{/m);
        if (!handleMatch) return null;
        const handleIdx = searchFrom + handleMatch.index;
        const indent = handleMatch[1];
        const markerBlock = `${indent}${ROUTE_MARKER}\n${indent}# === End App Routes ===\n\n`;
        return c.slice(0, handleIdx) + markerBlock + c.slice(handleIdx);
      });
      if (result.success) {
        log.info('caddy', 'Injected route markers into existing domain block', { domain });
      }
      return;
    }

    // No domain block at all — create one with dashboard + markers
    const dashboardRoot = platformPaths.sitePath('status');
    const apiPort = process.env.PORT || 3001;
    const block = `\n${domain} {\n  root * ${dashboardRoot}\n  encode gzip\n\n  handle /api/* {\n    reverse_proxy localhost:${apiPort}\n  }\n\n  handle /probe/* {\n    reverse_proxy localhost:${apiPort}\n  }\n\n  ${ROUTE_MARKER}\n  # === End App Routes ===\n\n  handle {\n    @notFile not file {path}\n    rewrite @notFile /index.html\n    file_server\n  }\n}\n`;

    const result = await caddy.modify(c => c + block);
    if (result.success) {
      log.info('caddy', 'Created main domain block with route markers', { domain });
    } else {
      throw new Error(`[DC-303] Failed to create main domain block for ${domain}: ${result.error}`);
    }
  }

  /** Inject a subpath config block between route markers in the Caddyfile. */
  async function addSubpathConfig(subdomain, configBlock) {
    const marker = `# --- DashCaddy: ${subdomain} ---`;
    const endMarker = `# --- End: ${subdomain} ---`;
    const END_ROUTE_MARKER = '# === End App Routes ===';

    const result = await caddy.modify(content => {
      if (content.includes(marker)) {
        log.info('caddy', 'Subpath config already exists, skipping', { subdomain });
        return null;
      }

      const endIdx = content.indexOf(END_ROUTE_MARKER);
      if (endIdx === -1) {
        throw new Error(`Route marker "${END_ROUTE_MARKER}" not found in Caddyfile`);
      }

      // Detect indentation from the end marker line
      const lineStart = content.lastIndexOf('\n', endIdx) + 1;
      const indent = content.slice(lineStart, endIdx).match(/^([ \t]*)/)?.[1] || '\t';

      const injection = `${indent}${marker}\n${configBlock}\n${indent}${endMarker}\n`;
      return content.slice(0, endIdx) + injection + content.slice(endIdx);
    });

    if (!result.success && result.error !== 'No changes to apply') {
      throw new Error(`[DC-303] Failed to add subpath config for ${subdomain}: ${result.error}`);
    }
    if (result.error === 'No changes to apply') {
      log.info('caddy', 'Subpath config already exists, reusing', { subdomain });
    }
  }

  /** Remove a subpath config block from between its markers in the Caddyfile. */
  async function removeSubpathConfig(subdomain) {
    const marker = `# --- DashCaddy: ${subdomain} ---`;
    const endMarker = `# --- End: ${subdomain} ---`;

    return await caddy.modify(content => {
      const startIdx = content.indexOf(marker);
      if (startIdx === -1) return null;

      const endIdx = content.indexOf(endMarker);
      if (endIdx === -1) return null;

      // Remove from the line start of the marker to the line end of the end marker
      const lineStart = content.lastIndexOf('\n', startIdx);
      const lineEnd = content.indexOf('\n', endIdx + endMarker.length);

      const modified = content.slice(0, lineStart) + content.slice(lineEnd);
      return modified.replace(/\n{3,}/g, '\n\n');
    });
  }

  return {
    checkPortConflicts,
    findExistingContainerByImage,
    toDockerDesktopPath,
    processTemplateVariables,
    waitForHealthCheck,
    addCaddyConfig,
    addSubpathConfig,
    removeSubpathConfig,
    ensureMainDomainBlock,
    RESERVED_SUBPATHS,
    generateStaticSiteConfig
  };
};
