import fs from 'fs';
import { execSync, spawn } from 'child_process';
import path from 'path';

import { TENANT_HOME_BASE } from './config.js';
import { Tenant } from './types.js';

export interface TenantPortRange {
  start: number | null;
  end: number | null;
  size: number | null;
  env: string | null;
}

export interface LiveContainer {
  name: string;
  port: number;
  app: string | null;   // app name if matches a registered service, null if zombie
  status: string;
}

export interface HostingSnapshot {
  tenantId: string | null;
  appsRoot: string;
  contractVersion: 7;
  portRange: TenantPortRange;
  usedPorts: number[];
  liveContainers: LiveContainer[];  // containers actually running in this tenant's port range
  commands: {
    appApply: 'app-apply';
    appRemove: 'app-remove';
  };
  paths: {
    proxyAppSpec: '/workspace/apps/<app>/app.yml';
    staticRootBase: '/workspace/apps/<app>/';
    ipcMessages: '/workspace/ipc/messages/';
    ipcErrors: '/workspace/ipc/errors/';
    hostedAppStatusDir: '/workspace/ipc/hosting-status/';
  };
  notes: string[];
}

export interface ComposeValidationOptions {
  tenantId: string;
  composeDir: string;
  composeContent: string;
  requiredHostPort?: number;
  portRange?: TenantPortRange;
}

export interface ComposeValidationResult {
  valid: boolean;
  errors: string[];
  hostPorts: number[];
}

export interface StoredTenantService {
  subdomain: string;
  config: Record<string, unknown>;
}

export function getStoredServicePorts(
  services: StoredTenantService[],
): number[] {
  const ports = new Set<number>();
  for (const service of services) {
    const port = Number(service.config?.port);
    if (Number.isInteger(port) && port > 0) {
      ports.add(port);
    }
  }
  return [...ports].sort((a, b) => a - b);
}

export function chooseAvailablePort(
  portRange: TenantPortRange,
  usedPorts: number[],
): number | null {
  if (portRange.start == null || portRange.end == null) return null;
  const used = new Set(usedPorts);
  for (let port = portRange.start; port <= portRange.end; port += 1) {
    if (!used.has(port)) return port;
  }
  return null;
}

export function sanitizeAppLabel(value: string): string {
  return String(value || '').replace(/[^a-zA-Z0-9-]/g, '');
}

export function getTenantAppComposeProjectName(
  tenantId: string,
  appName: string,
): string {
  const sanitizeProjectSegment = (value: string): string =>
    String(value || '')
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, '-')
      .replace(/^-+/, '')
      .replace(/-+$/, '')
      .replace(/-+/g, '-');

  const tenantSegment = sanitizeProjectSegment(tenantId);
  const appSegment = sanitizeProjectSegment(appName);
  return `nanoclaw-${tenantSegment}-${appSegment}`;
}

export function getTenantAppsRoot(tenantId: string): string {
  return path.join(TENANT_HOME_BASE, tenantId, 'workspace', 'apps');
}

export function getTenantAppDir(tenantId: string, appName: string): string {
  return path.join(getTenantAppsRoot(tenantId), sanitizeAppLabel(appName));
}

export function getTenantAppComposePath(
  tenantId: string,
  appName: string,
): string {
  return path.join(getTenantAppDir(tenantId, appName), 'docker-compose.yml');
}

export function getTenantPortRange(
  tenant?: Pick<Tenant, 'port_range_start' | 'port_range_size'>,
): TenantPortRange {
  const start = tenant?.port_range_start ?? null;
  const size = tenant?.port_range_size ?? null;
  const end = start != null && size != null ? start + size - 1 : null;
  return {
    start,
    end,
    size,
    env:
      start != null && end != null ? `${start}-${end}` : null,
  };
}

export async function buildHostingSnapshot(
  tenant?: Pick<Tenant, 'id' | 'port_range_start' | 'port_range_size'>,
): Promise<HostingSnapshot> {
  // Read used ports from the tenant's Caddy config — the single source of truth.
  // Every app that has been deployed has a reverse_proxy entry in this file.
  const usedPorts: number[] = [];
  const liveContainers: LiveContainer[] = [];
  if (tenant?.id) {
    try {
      const caddyFile = `/etc/caddy/tenants/${tenant.id}.caddy`;
      const caddyContent = fs.readFileSync(caddyFile, 'utf8');
      for (const m of caddyContent.matchAll(/(?:localhost|127\.0\.0\.1):(\d+)/g)) {
        const port = parseInt(m[1], 10);
        if (!usedPorts.includes(port)) usedPorts.push(port);
      }
    } catch { /* caddy file may not exist yet */ }

    // Build app→port map from Caddy config.
    // nerdctl rootless does not expose port bindings via nerdctl ps / inspect,
    // so we cross-reference container names with the Caddy subdomain entries.
    const appPortMap = new Map<string, number>();
    try {
      const caddyFile = `/etc/caddy/tenants/${tenant.id}.caddy`;
      const caddyLines = fs.readFileSync(caddyFile, 'utf8').split('\n');
      let currentApp: string | null = null;
      for (const line of caddyLines) {
        const hostMatch = line.match(/^([a-z0-9][a-z0-9-]*)\.(?:[a-z0-9.-]+)\s*\{/);
        if (hostMatch) {
          currentApp = hostMatch[1];
        } else if (currentApp) {
          const portMatch = line.match(/reverse_proxy\s+(?:localhost|127\.0\.0\.1):(\d+)/);
          if (portMatch) {
            appPortMap.set(currentApp, parseInt(portMatch[1], 10));
            currentApp = null;
          } else if (line.trim() === '}') {
            currentApp = null;
          }
        }
      }
    } catch { /* caddy file may not exist */ }

    // Query live containers via nanoclaw-tenant-runtime.
    // Ports are resolved from appPortMap since nerdctl rootless doesn't expose bindings.
    // async spawn avoids blocking the event loop; SIGKILL on timeout avoids orphaned sudo shells.
    // A pkill in finally cleans any nerdctl grandchildren that survive SIGKILL to sudo.
    const portRange = getTenantPortRange(tenant);
    let nerdctlStdout = '';
    const RUNTIME_HELPER = '/run/current-system/sw/bin/nanoclaw-tenant-runtime';
    try {
      nerdctlStdout = await new Promise<string>((resolve) => {
        const chunks: Buffer[] = [];
        const child = spawn(
          '/run/wrappers/bin/sudo',
          [RUNTIME_HELPER, tenant.id, 'nerdctl', 'ps', '--format', '{{.Names}}\t{{.Status}}'],
          { stdio: ['ignore', 'pipe', 'ignore'] },
        );
        child.stdout?.on('data', (d: Buffer) => chunks.push(d));
        const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(''); }, 10000);
        child.on('close', () => { clearTimeout(timer); resolve(Buffer.concat(chunks).toString('utf8')); });
        child.on('error', () => { clearTimeout(timer); resolve(''); });
      });
    } finally {
      // Kill any orphaned nerdctl ps processes for this tenant
      try {
        execSync(`/run/wrappers/bin/sudo pkill -KILL -u ${tenant.id} -f '_NERDCTL_INTERNAL_LOGGING'`, { timeout: 5000, stdio: 'ignore' });
      } catch { /* best effort */ }
    }
    if (nerdctlStdout) {
      const prefix = `nanoclaw-${tenant.id}-`;
      for (const line of nerdctlStdout.split('\n')) {
        const [name, status] = line.split('\t');
        if (!name || !status || !name.startsWith(prefix)) continue;
        const rest = name.slice(prefix.length);
        const parts = rest.split('-');
        // strip trailing service name and instance number (e.g. "web-1")
        const appName = parts.length >= 3 ? parts.slice(0, -2).join('-') : parts[0];
        const port = appPortMap.get(appName);
        if (port == null) continue;
        if (
          portRange.start != null &&
          portRange.end != null &&
          (port < portRange.start || port > portRange.end)
        ) continue;
        liveContainers.push({ name, port, app: appName, status: status.trim() });
      }
    }
  }

  return {
    tenantId: tenant?.id ?? null,
    appsRoot: '/workspace/apps',
    contractVersion: 7,
    portRange: getTenantPortRange(tenant),
    usedPorts: usedPorts.sort((a, b) => a - b),
    liveContainers,
    commands: {
      appApply: 'app-apply',
      appRemove: 'app-remove',
    },
    paths: {
      proxyAppSpec: '/workspace/apps/<app>/app.yml',
      staticRootBase: '/workspace/apps/<app>/',
      ipcMessages: '/workspace/ipc/messages/',
      ipcErrors: '/workspace/ipc/errors/',
      hostedAppStatusDir: '/workspace/ipc/hosting-status/',
    },
    notes: [
      'Proxy apps deploy from /workspace/apps/<app>/app.yml using tenant-local rootless containerd, buildkitd, and nerdctl.',
      'Do not create docker-compose.yml for new apps. The supported contract is app.yml plus app-apply.',
      'If a user asks to build, create, scaffold, run, deploy, host, or expose an app, site, frontend, backend, or API, treat that as a hosted-app request by default.',
      'When a user asks to build or host an app, the task is not complete until app.yml exists and app-apply has been sent, unless they explicitly asked for local-only files.',
      'Do not stop at local source files or tell the user to run the app themselves with npm start, node, Docker Compose, or similar handoff steps unless they explicitly asked for local-only work.',
      'Legacy nanoclaw.app.yml may still load as a compatibility fallback, but new hosting work must target app.yml and app-apply.',
      'Place hosted app source under /workspace/apps/<app>/, not /workspace/group/.',
      'Static apps are routed directly from files under /workspace/apps/',
      'Services in one proxy app run on a tenant-local rootless nerdctl bridge network and can reach each other by service name.',
      'The app.yml service contract supports image/build, internal_port, public, environment, volumes, depends_on, command, entrypoint, and sdk.',
      'Host port allocation is platform-managed. Do not add ports: bindings to app.yml.',
      'Tenant app images, layers, and runtime state live under the tenant home volume rather than the shared host Docker daemon.',
      'Read /workspace/ipc/errors/ after a failed deploy before changing app.yml again',
      'Read /workspace/ipc/hosting-status/ for recent unit state and logs after deploy attempts',
      'Verify deployed apps from the public URL, using the actual user-facing route when possible.',
      'Do not try to verify proxy apps via localhost:<hostPort> from inside the sandboxed agent container.',
      'Use /health only when the app explicitly defines that route; it is not a required part of the platform contract.',
    ],
  };
}

function extractHostPorts(composeContent: string): number[] {
  const ports = new Set<number>();
  const portBlocks =
    composeContent.match(
      /ports:\s*\n((?:\s+-\s*["']?[^"'\n]+["']?\s*\n?)*)/g,
    ) || [];

  for (const block of portBlocks) {
    const lines = block.match(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm) || [];
    for (const line of lines) {
      const raw = line.replace(/^\s*-\s*["']?/, '').replace(/["']?\s*$/, '');
      const segments = raw.split(':');
      const candidate =
        segments.length >= 2 ? segments[segments.length - 2] : segments[0];
      const hostPort = Number(candidate);
      if (!Number.isNaN(hostPort)) {
        ports.add(hostPort);
      }
    }
  }

  return Array.from(ports).sort((a, b) => a - b);
}

function extractVolumeSources(composeContent: string): string[] {
  const sources: string[] = [];
  const volumeBlocks =
    composeContent.match(
      /volumes:\s*\n((?:\s+-\s*["']?[^"'\n]+["']?\s*\n?)*)/g,
    ) || [];

  for (const block of volumeBlocks) {
    const lines = block.match(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm) || [];
    for (const line of lines) {
      const raw = line.replace(/^\s*-\s*["']?/, '').replace(/["']?\s*$/, '');
      const parts = raw.split(':');
      if (parts.length >= 2) {
        sources.push(parts[0]);
      }
    }
  }

  return sources;
}

function validateVolumeSources(
  tenantId: string,
  composeDir: string,
  sources: string[],
): string[] {
  const errors: string[] = [];
  const allowedPrefix = path.join(TENANT_HOME_BASE, tenantId) + path.sep;

  for (const source of sources) {
    if (!source) continue;
    if (source === '/var/run/docker.sock') {
      errors.push('Docker socket mounts are not allowed.');
      continue;
    }
    if (source.startsWith('./') || source.startsWith('../')) continue;
    if (!source.includes('/')) continue;
    const resolved = path.resolve(composeDir, source);
    if (!resolved.startsWith(allowedPrefix)) {
      errors.push(
        `Volume mount "${source}" resolves outside the tenant home and is not allowed.`,
      );
    }
  }

  return errors;
}


export function validateTenantCompose(
  options: ComposeValidationOptions,
): ComposeValidationResult {
  const { tenantId, composeDir, composeContent, requiredHostPort, portRange } =
    options;
  const errors: string[] = [];

  if (/^\s*network_mode:\s*["']?host["']?\s*$/m.test(composeContent)) {
    errors.push('network_mode: host is not allowed.');
  }
  if (/^\s*network_mode:\s*/m.test(composeContent)) {
    errors.push('network_mode is not allowed.');
  }
  if (/^\s*privileged:\s*true\s*$/m.test(composeContent)) {
    errors.push('privileged containers are not allowed.');
  }
  if (/^\s*pid:\s*["']?host["']?\s*$/m.test(composeContent)) {
    errors.push('pid: host is not allowed.');
  }
  if (/^\s*cap_add:\s*$/m.test(composeContent)) {
    errors.push('cap_add is not allowed.');
  }
  if (/^\s*networks:\s*$/m.test(composeContent)) {
    errors.push('Custom Docker networks are not allowed.');
  }
  if (/^\s{4,}networks:\s*$/m.test(composeContent)) {
    errors.push('Service-level network configuration is not allowed.');
  }
  if (composeContent.includes('/var/run/docker.sock')) {
    errors.push('Docker socket mounts are not allowed.');
  }

  const hostPorts = extractHostPorts(composeContent);
  if (portRange?.start != null && portRange?.end != null) {
    for (const hostPort of hostPorts) {
      if (hostPort < portRange.start || hostPort > portRange.end) {
        errors.push(
          `Host port ${hostPort} is outside the allocated tenant range ${portRange.start}-${portRange.end}.`,
        );
      }
    }
  }

  if (
    requiredHostPort != null &&
    !hostPorts.includes(requiredHostPort)
  ) {
    errors.push(
      `Compose file must expose host port ${requiredHostPort} to match the requested proxy route.`,
    );
  }

  const sources = extractVolumeSources(composeContent);
  errors.push(...validateVolumeSources(tenantId, composeDir, sources));

  return {
    valid: errors.length === 0,
    errors,
    hostPorts,
  };
}

export function resolveServiceRemovalSubdomains(
  storedServices: StoredTenantService[],
  requestedSubdomains: string[],
  requestedService?: string,
): string[] {
  const subdomains = new Set<string>();

  for (const raw of requestedSubdomains) {
    const subdomain = sanitizeAppLabel(raw);
    if (subdomain) {
      subdomains.add(subdomain);
    }
  }

  const serviceName = requestedService
    ? sanitizeAppLabel(requestedService)
    : '';
  if (serviceName) {
    for (const { subdomain, config } of storedServices) {
      const storedName = sanitizeAppLabel(String(config.name || subdomain));
      if (storedName === serviceName || subdomain === serviceName) {
        subdomains.add(subdomain);
      }
    }
  }

  return Array.from(subdomains).sort();
}
