import Handlebars from 'handlebars';
import fs from 'fs/promises';
import path from 'path';
import { env } from '../config/env';
import { logger } from '../utils/logger';

// Register helpers
Handlebars.registerHelper('ifEq', function (this: unknown, a: unknown, b: unknown, options: Handlebars.HelperOptions) {
  return a === b ? options.fn(this) : options.inverse(this);
});

Handlebars.registerHelper('unless', function (this: unknown, condition: unknown, options: Handlebars.HelperOptions) {
  return !condition ? options.fn(this) : options.inverse(this);
});

Handlebars.registerHelper('now', function () {
  return new Date().toISOString();
});

Handlebars.registerHelper('math', function (a: number, op: string, b: number) {
  switch (op) {
    case '+': return a + b;
    case '-': return a - b;
    default: return a;
  }
});

export interface TemplateContext {
  // Instance identity
  slug: string;
  name: string;
  domain: string;
  containerPrefix: string;   // e.g., "cml-better-edmonton"
  networkName: string;        // e.g., "cml-better-edmonton"
  composeProject: string;     // e.g., "cml-better-edmonton"

  // Ports
  ports: {
    api: number;
    admin: number;
    postgres: number;
    nginx: number;
    embed: number;
  };

  // Secrets (decrypted)
  secrets: {
    postgresPassword: string;
    redisPassword: string;
    jwtAccessSecret: string;
    jwtRefreshSecret: string;
    jwtInviteSecret: string;
    encryptionKey: string;
    initialAdminPassword: string;
    adminEmail: string;
    nocodbAdminPassword: string;
    grafanaAdminPassword: string;
    listmonkAdminPassword: string;
    listmonkApiToken: string;
    giteaAdminPassword: string;
    n8nEncryptionKey: string;
    gancioAdminPassword: string;
    vaultwardenAdminToken: string;
    jitsiAppSecret: string;
    jitsiJicofoAuthPassword: string;
    jitsiJvbAuthPassword: string;
    rocketchatAdminPassword: string;
  };

  // Feature flags
  enableMedia: boolean;
  enableChat: boolean;
  enableGancio: boolean;
  enableListmonk: boolean;
  enableMonitoring: boolean;
  enableDevTools: boolean;
  enablePayments: boolean;
  enableMeet: boolean;
  enableSms: boolean;
  enableSocial: boolean;
  enablePeople: boolean;
  enableAnalytics: boolean;
  jvbAdvertiseIp: string;
  enablePangolin: boolean;

  // Pangolin tunnel
  pangolin: {
    endpoint: string;
    newtId: string;
    newtSecret: string;
  };

  // SMTP
  smtpHost: string;
  smtpPort: number;
  smtpUser: string;
  smtpFrom: string;
  emailTestMode: boolean;

  // Git
  gitBranch: string;

  // Container Registry
  registryUrl: string;
  useRegistry: boolean;
  imageTag: string;
}

/** Subset of Instance fields needed to build a TemplateContext. */
export interface InstanceForTemplate {
  slug: string;
  name: string;
  domain: string;
  composeProject: string;
  portConfig: unknown;
  enableMedia: boolean;
  enableChat: boolean;
  enableGancio: boolean;
  enableListmonk: boolean;
  enableMonitoring: boolean;
  enableDevTools: boolean;
  enablePayments: boolean;
  enableMeet: boolean;
  enableSms: boolean;
  enableSocial: boolean;
  enablePeople: boolean;
  enableAnalytics: boolean;
  jvbAdvertiseIp: string | null;
  pangolinEndpoint: string | null;
  pangolinNewtId: string | null;
  pangolinNewtSecret: string | null;
  smtpHost: string | null;
  smtpPort: number | null;
  smtpUser: string | null;
  smtpFrom: string | null;
  emailTestMode: boolean;
  gitBranch: string;
  // Per-instance image tag override (Approach C). NULL falls back to env.IMAGE_TAG.
  imageTag: string | null;
}

/**
 * Build the TemplateContext from an instance record and its decrypted secrets.
 */
export function buildTemplateContext(
  instance: InstanceForTemplate,
  secrets: Record<string, string>
): TemplateContext {
  const ports = instance.portConfig as Record<string, number>;
  return {
    slug: instance.slug,
    name: instance.name,
    domain: instance.domain,
    containerPrefix: instance.composeProject,
    networkName: instance.composeProject,
    composeProject: instance.composeProject,
    ports: {
      api: ports.api,
      admin: ports.admin,
      postgres: ports.postgres,
      nginx: ports.nginx,
      embed: ports.embed || (ports.nginx || 80) + 1,
    },
    secrets: {
      postgresPassword: secrets.postgresPassword,
      redisPassword: secrets.redisPassword,
      jwtAccessSecret: secrets.jwtAccessSecret,
      jwtRefreshSecret: secrets.jwtRefreshSecret,
      jwtInviteSecret: secrets.jwtInviteSecret || secrets.jwtAccessSecret,
      encryptionKey: secrets.encryptionKey,
      initialAdminPassword: secrets.initialAdminPassword,
      adminEmail: secrets.adminEmail,
      nocodbAdminPassword: secrets.nocodbAdminPassword,
      grafanaAdminPassword: secrets.grafanaAdminPassword,
      listmonkAdminPassword: secrets.listmonkAdminPassword,
      listmonkApiToken: secrets.listmonkApiToken,
      giteaAdminPassword: secrets.giteaAdminPassword,
      n8nEncryptionKey: secrets.n8nEncryptionKey,
      gancioAdminPassword: secrets.gancioAdminPassword,
      vaultwardenAdminToken: secrets.vaultwardenAdminToken,
      jitsiAppSecret: secrets.jitsiAppSecret || '',
      jitsiJicofoAuthPassword: secrets.jitsiJicofoAuthPassword || '',
      jitsiJvbAuthPassword: secrets.jitsiJvbAuthPassword || '',
      rocketchatAdminPassword: secrets.rocketchatAdminPassword || '',
    },
    enableMedia: instance.enableMedia,
    enableChat: instance.enableChat,
    enableGancio: instance.enableGancio,
    enableListmonk: instance.enableListmonk,
    enableMonitoring: instance.enableMonitoring,
    enableDevTools: instance.enableDevTools,
    enablePayments: instance.enablePayments,
    enableMeet: instance.enableMeet,
    enableSms: instance.enableSms,
    enableSocial: instance.enableSocial,
    enablePeople: instance.enablePeople,
    enableAnalytics: instance.enableAnalytics,
    jvbAdvertiseIp: instance.jvbAdvertiseIp || '',
    enablePangolin: !!(instance.pangolinEndpoint && instance.pangolinNewtId && instance.pangolinNewtSecret),
    pangolin: {
      endpoint: instance.pangolinEndpoint || '',
      newtId: instance.pangolinNewtId || '',
      newtSecret: instance.pangolinNewtSecret || '',
    },
    smtpHost: instance.smtpHost || '',
    smtpPort: instance.smtpPort || 587,
    smtpUser: instance.smtpUser || '',
    smtpFrom: instance.smtpFrom || '',
    emailTestMode: instance.emailTestMode,
    gitBranch: instance.gitBranch,
    registryUrl: env.GITEA_REGISTRY,
    useRegistry: env.USE_REGISTRY_IMAGES,
    // Approach C: per-instance imageTag overrides the CCP-wide env default.
    // NULL on the Instance row falls back to env.IMAGE_TAG (typically 'latest').
    imageTag: instance.imageTag || env.IMAGE_TAG,
  };
}

const templateCache = new Map<string, HandlebarsTemplateDelegate>();

async function loadTemplate(templatePath: string): Promise<HandlebarsTemplateDelegate> {
  if (templateCache.has(templatePath)) {
    return templateCache.get(templatePath)!;
  }

  const source = await fs.readFile(templatePath, 'utf-8');
  const compiled = Handlebars.compile(source, { noEscape: true });
  templateCache.set(templatePath, compiled);
  return compiled;
}

export async function renderTemplate(templateName: string, context: TemplateContext): Promise<string> {
  const templatesDir = path.resolve(__dirname, '../..', 'templates');
  const templatePath = path.join(templatesDir, templateName);
  const template = await loadTemplate(templatePath);
  return template(context);
}

export async function renderAllTemplates(context: TemplateContext, outputDir: string): Promise<void> {
  // Always read fresh templates from disk — provisioning is infrequent and
  // templates may have changed since the API last cached them.
  clearTemplateCache();

  const templatesDir = path.resolve(__dirname, '../..', 'templates');

  // Templates that produce on-disk files actually consumed by tenant containers.
  // nginx/conf.d/* templates removed 2026-05-23: the new (prod-style)
  // docker-compose.yml does NOT mount conf.d/ into the nginx container
  // ("Note: conf.d is NOT mounted (configs are generated at startup from
  // templates)" — nginx confs are baked into the nginx Docker image).
  // Writing them was a no-op orphan.
  const templateFiles = [
    { template: 'docker-compose.yml.hbs', output: 'docker-compose.yml' },
    { template: 'env.hbs', output: '.env' },
    { template: 'configs/pangolin/resources.yml.hbs', output: 'configs/pangolin/resources.yml' },
    { template: 'configs/prometheus/prometheus.yml.hbs', output: 'configs/prometheus/prometheus.yml' },
    { template: 'configs/grafana/datasources/datasources.yml.hbs', output: 'configs/grafana/datasources/datasources.yml' },
  ];

  for (const { template, output } of templateFiles) {
    const templatePath = path.join(templatesDir, template);
    try {
      await fs.access(templatePath);
    } catch {
      logger.warn(`Template not found: ${template}, skipping`);
      continue;
    }

    const rendered = await renderTemplate(template, context);
    const outputPath = path.join(outputDir, output);

    // Ensure output directory exists
    await fs.mkdir(path.dirname(outputPath), { recursive: true });
    await fs.writeFile(outputPath, rendered, 'utf-8');
    logger.debug(`Rendered ${template} → ${outputPath}`);
  }

  // Copy static files (no templating needed). nginx/nginx.conf removed
  // 2026-05-23 — same reason as nginx/conf.d/* in templateFiles above
  // (nginx image bakes its own; on-disk file is an orphan).
  const staticFiles = [
    'configs/prometheus/alerts.yml',
    'configs/alertmanager/alertmanager.yml',
    'configs/grafana/dashboards/dashboards.yml',
    'configs/grafana/dashboards/application-overview.json',
    'configs/grafana/dashboards/api-performance.json',
    'configs/grafana/dashboards/system-health.json',
  ];
  for (const file of staticFiles) {
    const srcPath = path.join(templatesDir, file);
    try {
      await fs.access(srcPath);
      const destPath = path.join(outputDir, file);
      await fs.mkdir(path.dirname(destPath), { recursive: true });
      await fs.copyFile(srcPath, destPath);
    } catch {
      logger.warn(`Static file not found: ${file}, skipping`);
    }
  }
}

export function clearTemplateCache(): void {
  templateCache.clear();
}

/**
 * Render all templates in memory and return them as an array of { relativePath, content }.
 * Used for remote instances where we can't write to the local filesystem — rendered
 * files are sent to the remote agent via HTTP instead.
 */
export async function renderAllTemplatesInMemory(
  context: TemplateContext
): Promise<Array<{ relativePath: string; content: string }>> {
  clearTemplateCache();
  const templatesDir = path.resolve(__dirname, '../..', 'templates');
  const result: Array<{ relativePath: string; content: string }> = [];

  // Templates that produce on-disk files actually consumed by tenant containers.
  // nginx/conf.d/* templates removed 2026-05-23: the new (prod-style)
  // docker-compose.yml does NOT mount conf.d/ into the nginx container
  // ("Note: conf.d is NOT mounted (configs are generated at startup from
  // templates)" — nginx confs are baked into the nginx Docker image).
  // Writing them was a no-op orphan.
  const templateFiles = [
    { template: 'docker-compose.yml.hbs', output: 'docker-compose.yml' },
    { template: 'env.hbs', output: '.env' },
    { template: 'configs/pangolin/resources.yml.hbs', output: 'configs/pangolin/resources.yml' },
    { template: 'configs/prometheus/prometheus.yml.hbs', output: 'configs/prometheus/prometheus.yml' },
    { template: 'configs/grafana/datasources/datasources.yml.hbs', output: 'configs/grafana/datasources/datasources.yml' },
  ];

  for (const { template, output } of templateFiles) {
    const templatePath = path.join(templatesDir, template);
    try {
      await fs.access(templatePath);
    } catch {
      logger.warn(`Template not found: ${template}, skipping`);
      continue;
    }
    const rendered = await renderTemplate(template, context);
    result.push({ relativePath: output, content: rendered });
  }

  // Read static files into memory. nginx/nginx.conf removed 2026-05-23 —
  // same orphan reason as in renderAllTemplates above.
  const staticFiles = [
    'configs/prometheus/alerts.yml',
    'configs/alertmanager/alertmanager.yml',
    'configs/grafana/dashboards/dashboards.yml',
    'configs/grafana/dashboards/application-overview.json',
    'configs/grafana/dashboards/api-performance.json',
    'configs/grafana/dashboards/system-health.json',
  ];
  for (const file of staticFiles) {
    const srcPath = path.join(templatesDir, file);
    try {
      const content = await fs.readFile(srcPath, 'utf-8');
      result.push({ relativePath: file, content });
    } catch {
      logger.warn(`Static file not found: ${file}, skipping`);
    }
  }

  return result;
}
