const express = require('express');
const path = require('path');
const fs = require('fs');
const { DOCKER } = require('../../src/utilities/constants');
const { ok, validationError, notFound, errorResponse } = require('../../src/utils/responses');

const DEFAULT_BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');

/**
 * Apps restore routes factory
 * @param {Object} deps - Explicit dependencies
 * @param {Object} deps.docker - Docker client wrapper
 * @param {Object} deps.caddy - Caddy client
 * @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
 * @param {Object} deps.helpers - Apps helpers module
 * @param {Object} deps.APP_TEMPLATES - App templates registry
 * @param {Object} deps.dns - DNS client
 * @param {Function} deps.buildServiceUrl - Service URL builder
 * @returns {express.Router}
 */
module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, errorResponse, log, helpers, APP_TEMPLATES, dns, buildServiceUrl }) {
  const router = express.Router();

  const ctx = {
    APP_TEMPLATES,
    dns,
    buildServiceUrl
  };

  /**
   * Restore a single service from its deployment manifest.
   * Pulls image, creates container, starts it, recreates Caddy config.
   * Skips if container is already running.
   */
  router.post('/:appId/restore', asyncHandler(async (req, res) => {
    const { appId } = req.params;
    const services = await servicesStateManager.read();
    const service = services.find(s => s.id === appId);

    if (!service) {
      return errorResponse(res, 404, `Service "${appId}" not found in services.json`);
    }
    if (!service.deploymentManifest) {
      return errorResponse(res, 400, `Service "${appId}" has no deployment manifest — it was deployed before the manifest feature was added. Redeploy it manually to create a manifest.`);
    }

    const result = await restoreService(service);
    ok(res, { result });
  }, 'apps-restore'));

  /**
   * Restore all services that have deployment manifests.
   * Returns per-service results.
   */
  router.post('/restore-all', asyncHandler(async (req, res) => {
    const services = await servicesStateManager.read();
    const restoreable = services.filter(s => s.deploymentManifest);

    if (restoreable.length === 0) {
      return ok(res, {
        message: 'No services have deployment manifests to restore',
        results: []
      });
    }

    const results = [];
    for (const service of restoreable) {
      try {
        const result = await restoreService(service);
        results.push(result);
      } catch (error) {
        results.push({
          id: service.id,
          name: service.name,
          status: 'failed',
          error: error.message
        });
      }
    }

    const succeeded = results.filter(r => r.status === 'restored').length;
    const skipped = results.filter(r => r.status === 'skipped').length;
    const failed = results.filter(r => r.status === 'failed').length;

    ok(res, {
      message: `Restore complete: ${succeeded} restored, ${skipped} skipped, ${failed} failed`,
      results
    });
  }, 'apps-restore-all'));

  /**
   * List all services and their restore status.
   */
  router.get('/restore-status', asyncHandler(async (req, res) => {
    const services = await servicesStateManager.read();
    const status = [];

    for (const service of services) {
      const entry = {
        id: service.id,
        name: service.name,
        hasManifest: !!service.deploymentManifest,
        templateId: service.deploymentManifest?.templateId || service.appTemplate || null,
        deployedAt: service.deployedAt || null,
        containerRunning: false
      };

      // Check if container is currently running
      if (service.containerId) {
        try {
          const container = docker.client.getContainer(service.containerId);
          const info = await container.inspect();
          entry.containerRunning = info.State.Running;
        } catch (e) {
          entry.containerRunning = false;
        }
      }

      status.push(entry);
    }

    ok(res, { services: status });
  }, 'apps-restore-status'));

  // ==================== POINT-IN-TIME RESTORE (BACKUP FILE BASED) ====================

  // Get available backup files for a specific app
  router.get('/:appId/backup-points', asyncHandler(async (req, res) => {
    const { appId } = req.params;
    const backupDir = DEFAULT_BACKUP_DIR;
    const files = [];

    try {
      if (fs.existsSync(backupDir)) {
        const entries = fs.readdirSync(backupDir, { withFileTypes: true });

        for (const entry of entries) {
          if (entry.isFile() && entry.name.endsWith('.backup')) {
            try {
              const nameWithoutExt = entry.name.replace('.backup', '');
              const parts = nameWithoutExt.split('-');
              const fileAppId = parts[0];

              // Only include files for the requested app
              if (fileAppId !== appId) continue;

              const filepath = path.join(backupDir, entry.name);
              const stats = fs.statSync(filepath);
              const timestamp = parts.length > 1 ? parseInt(parts[parts.length - 1]) : stats.mtimeMs;

              files.push({
                name: entry.name,
                appId: fileAppId,
                size: stats.size,
                sizeFormatted: formatBytes(stats.size),
                timestamp: new Date(timestamp).toISOString(),
                modified: stats.mtime.toISOString(),
                path: filepath
              });
            } catch (err) {
              // Skip malformed filenames
            }
          }
        }
      }
    } catch (err) {
      // Directory might not exist yet
    }

    // Sort by timestamp descending (newest first)
    files.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

    ok(res, {
      appId,
      isBackupFile: true,
      files,
      total: files.length
    });
  }, 'apps-backup-points'));

  // Revert a specific app to a backup file (point-in-time restore)
  router.post('/:appId/revert/:filename', asyncHandler(async (req, res) => {
    const { appId, filename } = req.params;
    const { encryptionKey, restartContainers } = req.body || {};

    // Security: prevent path traversal
    if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
      return validationError(res, 'Invalid filename');
    }

    const filepath = path.join(DEFAULT_BACKUP_DIR, filename);
    if (!fs.existsSync(filepath)) {
      return notFound(res, `Backup file not found: ${filename}`);
    }

    try {
      // Read the backup file
      let fileData = fs.readFileSync(filepath);

      // Decrypt if needed
      if (encryptionKey) {
        try {
          fileData = await backupManager.decryptBackup(fileData, encryptionKey);
        } catch (err) {
          return validationError(res, 'Failed to decrypt backup: ' + err.message);
        }
      }

      // Decompress
      const backupData = await backupManager.decompressBackup(fileData);

      // Extract to temp directory
      const os = require('os');
      const crypto = require('crypto');
      const tempDir = path.join(os.tmpdir(), `dashcaddy-revert-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`);
      fs.mkdirSync(tempDir, { recursive: true });

      try {
        const tarPath = path.join(tempDir, 'backup.tar.gz');
        fs.writeFileSync(tarPath, backupData);

        const { execSync } = require('child_process');
        try {
          execSync(`cd "${tempDir}" && tar -xzf backup.tar.gz`, { stdio: 'pipe' });
        } catch (tarErr) {
          throw new Error('Failed to extract backup archive: ' + tarErr.message);
        }

        // Read manifest if present
        let manifest = null;
        const manifestPath = path.join(tempDir, 'manifest.json');
        if (fs.existsSync(manifestPath)) {
          try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch (_) {}
        }

        // Read app-specific data
        const appServicesPath = path.join(tempDir, 'services.json');
        const appConfigPath = path.join(tempDir, 'config.json');
        const appCredsPath = path.join(tempDir, 'credentials.json');

        let restoreData = { services: null, config: null, credentials: null };

        if (fs.existsSync(appServicesPath)) {
          try { restoreData.services = JSON.parse(fs.readFileSync(appServicesPath, 'utf8')); } catch (_) {}
        }
        if (fs.existsSync(appConfigPath)) {
          try { restoreData.config = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')); } catch (_) {}
        }
        if (fs.existsSync(appCredsPath)) {
          try { restoreData.credentials = JSON.parse(fs.readFileSync(appCredsPath, 'utf8')); } catch (_) {}
        }

        // If restartContainers is true, actually perform the restore
        if (restartContainers) {
          if (restoreData.services) backupManager.restoreServices(restoreData.services);
          if (restoreData.config) backupManager.restoreConfig(restoreData.config);
          if (restoreData.credentials) backupManager.restoreCredentials(restoreData.credentials);

          // Cleanup temp dir
          fs.rmSync(tempDir, { recursive: true, force: true });

          ok(res, {
            isBackupFile: true,
            restored: {
              services: !!restoreData.services,
              config: !!restoreData.config,
              credentials: !!restoreData.credentials
            },
            message: `${appId} reverted to backup successfully`
          });
        } else {
          // Preview mode
          fs.rmSync(tempDir, { recursive: true, force: true });
          ok(res, {
            isBackupFile: true,
            preview: true,
            filename,
            appId,
            manifest,
            restoreData: {
              hasServices: !!restoreData.services,
              hasConfig: !!restoreData.config,
              hasCredentials: !!restoreData.credentials
            }
          });
        }
      } catch (err) {
        try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (_) {}
        throw err;
      }
    } catch (err) {
      errorResponse(res, 500, err.message);
    }
  }, 'apps-revert'));

  /**
   * Core restore logic for a single service.
   */
  async function restoreService(service) {
    const manifest = service.deploymentManifest;
    const template = ctx.APP_TEMPLATES[manifest.templateId];

    log.info('restore', `Restoring service: ${service.name}`, { id: service.id, templateId: manifest.templateId });

    // Static sites: just recreate Caddy config
    if (template?.isStaticSite) {
      log.info('restore', `Restoring static site Caddy config: ${service.name}`);
      // Static site Caddy config would need to be regenerated
      // For now, just confirm the service entry exists
      return {
        id: service.id,
        name: service.name,
        status: 'restored',
        type: 'static',
        message: `Static site "${service.name}" config preserved`
      };
    }

    // Docker container: check if already running
    if (service.containerId) {
      try {
        const existing = docker.client.getContainer(service.containerId);
        const info = await existing.inspect();
        if (info.State.Running) {
          log.info('restore', `Container already running, skipping: ${service.name}`);
          return {
            id: service.id,
            name: service.name,
            status: 'skipped',
            message: 'Container already running'
          };
        }
      } catch (e) {
        // Container doesn't exist — proceed with restore
      }
    }

    // Also check by name (container ID may have changed)
    const containerName = `${DOCKER.CONTAINER_PREFIX}${manifest.config.subdomain}`;
    try {
      const byName = docker.client.getContainer(containerName);
      const info = await byName.inspect();
      if (info.State.Running) {
        // Update the service entry with the current container ID
        await servicesStateManager.update(services => {
          const svc = services.find(s => s.id === service.id);
          if (svc) svc.containerId = info.Id;
          return services;
        });
        return {
          id: service.id,
          name: service.name,
          status: 'skipped',
          message: 'Container already running (found by name)'
        };
      }
      // Exists but not running — remove stale container
      await byName.remove({ force: true });
    } catch (e) {
      // Container doesn't exist — proceed
    }

    if (!manifest.container) {
      return {
        id: service.id,
        name: service.name,
        status: 'failed',
        error: 'No container configuration in manifest'
      };
    }

    // Pull image
    log.info('restore', `Pulling image: ${manifest.container.image}`);
    try {
      await docker.pull(manifest.container.image);
    } catch (e) {
      // Check if image exists locally
      const images = await docker.client.listImages({
        filters: { reference: [manifest.container.image] }
      });
      if (images.length === 0) {
        throw new Error(`Failed to pull image ${manifest.container.image}: ${e.message}`);
      }
      log.warn('restore', `Pull failed, using local image: ${manifest.container.image}`);
    }

    // Build container config from manifest
    const containerConfig = {
      Image: manifest.container.image,
      name: containerName,
      ExposedPorts: {},
      HostConfig: {
        PortBindings: {},
        Binds: manifest.container.volumes || [],
        RestartPolicy: { Name: 'unless-stopped' },
        LogConfig: DOCKER.LOG_CONFIG
      },
      Env: Object.entries(manifest.container.environment || {}).map(([k, v]) => `${k}=${v}`),
      Labels: {
        'sami.managed': 'true',
        'sami.app': manifest.templateId,
        'sami.subdomain': manifest.config.subdomain,
        'sami.deployed': new Date().toISOString(),
        'sami.restored': 'true'
      }
    };

    // Set up port bindings
    (manifest.container.ports || []).forEach(portMapping => {
      const [hostPort, containerPort, protocol = 'tcp'] = portMapping.split(/[:/]/);
      const containerPortKey = `${containerPort}/${protocol}`;
      containerConfig.ExposedPorts[containerPortKey] = {};
      containerConfig.HostConfig.PortBindings[containerPortKey] = [{ HostPort: hostPort }];
    });

    if (manifest.container.capabilities) {
      containerConfig.HostConfig.CapAdd = manifest.container.capabilities;
    }

    // Create and start container
    log.info('restore', `Creating container: ${containerName}`);
    const container = await docker.client.createContainer(containerConfig);
    await container.start();
    log.info('restore', `Container started: ${containerName}`);

    // Recreate Caddy config
    const port = manifest.config.port;
    const caddyOptions = {
      tailscaleOnly: manifest.caddy.tailscaleOnly,
      allowedIPs: manifest.caddy.allowedIPs,
      subpathSupport: manifest.caddy.subpathSupport,
    };

    if (manifest.caddy.routingMode === 'subdirectory') {
      const caddyConfig = caddy.generateConfig(manifest.config.subdomain, manifest.config.ip, port, caddyOptions);
      try {
        await helpers.ensureMainDomainBlock();
        await helpers.addSubpathConfig(manifest.config.subdomain, caddyConfig);
      } catch (e) {
        log.warn('restore', `Caddy config may already exist: ${e.message}`);
      }
    } else {
      const caddyConfig = caddy.generateConfig(manifest.config.subdomain, manifest.config.ip, port, caddyOptions);
      try {
        await helpers.addCaddyConfig(manifest.config.subdomain, caddyConfig);
      } catch (e) {
        log.warn('restore', `Caddy config may already exist: ${e.message}`);
      }
    }

    // DNS record
    if (manifest.config.createDns && manifest.caddy.routingMode !== 'subdirectory') {
      try {
        await ctx.dns.universalCreateRecord(manifest.config.subdomain, manifest.config.ip);
        log.info('restore', 'DNS record recreated', { subdomain: manifest.config.subdomain });
      } catch (e) {
        log.warn('restore', `DNS recreation failed: ${e.message}`);
      }
    }

    // Update the service entry with the new container ID
    await servicesStateManager.update(services => {
      const svc = services.find(s => s.id === service.id);
      if (svc) {
        svc.containerId = container.id;
        svc.url = buildServiceUrl(manifest.config.subdomain);
      }
      return services;
    });

    return {
      id: service.id,
      name: service.name,
      status: 'restored',
      type: 'container',
      containerId: container.id,
      message: `${service.name} restored successfully`
    };
  }

  return router;
};

// Helper: format bytes to human readable
function formatBytes(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
