import fs from 'node:fs';
import path from 'node:path';
import YAML from 'yaml';
import {
  ApplicationsFileSchema,
  ProvidersFileSchema,
  RoutingFileSchema,
  ZyphonFileSchema,
  PoliciesFileSchema,
  DltFileSchema,
  TemplatesFileSchema,
  SuppressionsFileSchema,
  AlertsFileSchema,
  TeamFileSchema,
  type ApplicationConfigModel,
  type ProviderConfigModel,
  type RoutingConfigModel,
  type ZyphonServerConfigModel,
  type PoliciesConfigModel,
  type DltConfigModel,
  type MessageTemplateModel,
  type SuppressionsFileModel,
  type AlertsFileModel,
  type TeamFileModel,
} from '@zyphon/config-schema';
import { interpolateEnvVars } from './interpolator.js';

export interface ZyphonClusterConfig {
  server: ZyphonServerConfigModel;
  applications: ApplicationConfigModel[];
  providers: ProviderConfigModel[];
  routing: RoutingConfigModel;
  policies: PoliciesConfigModel;
  loaded_at: number;
}

export class ConfigLoader {
  private configDir: string;
  private stateDir: string;

  constructor(configDir = './config', stateDir = './data/state') {
    this.configDir = path.resolve(configDir);
    this.stateDir = path.resolve(stateDir);
  }

  private readYamlFile(filename: string): unknown {
    const fullPath = path.join(this.configDir, filename);
    if (!fs.existsSync(fullPath)) {
      // Check if example file exists as fallback
      const examplePath = path.join(this.configDir, filename.replace(/\.yaml$/, '.example.yaml'));
      if (fs.existsSync(examplePath)) {
        const raw = fs.readFileSync(examplePath, 'utf8');
        return YAML.parse(raw);
      }
      return null;
    }
    const raw = fs.readFileSync(fullPath, 'utf8');
    return YAML.parse(raw);
  }

  /**
   * Load only providers.yaml (independent of applications/routing validation).
   * Provider CRUD must use this so a broken applications.yaml cannot block name/config saves.
   */
  public loadProvidersFile(env = process.env): ProviderConfigModel[] {
    const fullPath = path.join(this.configDir, 'providers.yaml');
    const rawProviders = this.readYamlFile('providers.yaml');
    // Never treat a missing/unreadable file as an empty list — that would let
    // create/update overwrite a good providers.yaml with a single new entry.
    if (rawProviders == null) {
      if (fs.existsSync(fullPath)) {
        throw new Error(`providers.yaml exists but could not be parsed at ${fullPath}`);
      }
      return [];
    }
    const interpolatedProviders = interpolateEnvVars(rawProviders, env);
    return ProvidersFileSchema.parse(interpolatedProviders).providers;
  }

  /**
   * Load and validate all configuration files into a unified cluster configuration object.
   */
  public loadClusterConfig(env = process.env): ZyphonClusterConfig {
    // 1. zyphon.yaml
    const rawZyphon = this.readYamlFile('zyphon.yaml') ?? {};
    const interpolatedZyphon = interpolateEnvVars(rawZyphon, env);
    const parsedZyphon = ZyphonFileSchema.parse(interpolatedZyphon);

    // 2. applications.yaml
    const rawApps = this.readYamlFile('applications.yaml') ?? { applications: [] };
    const interpolatedApps = interpolateEnvVars(rawApps, env);
    const parsedApps = ApplicationsFileSchema.parse(interpolatedApps);

    // 3. providers.yaml
    const rawProviders = this.readYamlFile('providers.yaml') ?? { providers: [] };
    const interpolatedProviders = interpolateEnvVars(rawProviders, env);
    const parsedProviders = ProvidersFileSchema.parse(interpolatedProviders);

    // 4. routing.yaml
    const rawRouting = this.readYamlFile('routing.yaml') ?? {
      routing: {
        default: {
          email: ['smtp-primary'],
          sms: ['twilio-primary'],
          whatsapp: ['meta-primary'],
        },
      },
    };
    const interpolatedRouting = interpolateEnvVars(rawRouting, env);
    const parsedRouting = RoutingFileSchema.parse(interpolatedRouting);

    // 5. policies.yaml
    const rawPolicies = this.readYamlFile('policies.yaml') ?? { policies: {} };
    const interpolatedPolicies = interpolateEnvVars(rawPolicies, env);
    const parsedPolicies = PoliciesFileSchema.parse(interpolatedPolicies);

    // 6. Cross-reference integrity check
    this.validateIntegrity(
      parsedApps.applications,
      parsedProviders.providers,
      parsedRouting.routing,
    );

    return {
      server: parsedZyphon.server,
      applications: parsedApps.applications,
      providers: parsedProviders.providers,
      routing: parsedRouting.routing,
      policies: parsedPolicies.policies,
      loaded_at: Date.now(),
    };
  }

  /**
   * Cross-reference validation: ensure all referenced providers exist and match channel types.
   */
  public validateIntegrity(
    applications: ApplicationConfigModel[],
    providers: ProviderConfigModel[],
    routing: RoutingConfigModel,
  ): void {
    const providerMap = new Map<string, ProviderConfigModel>();
    for (const p of providers) {
      if (providerMap.has(p.id)) {
        throw new Error(`Duplicate provider id: ${p.id}`);
      }
      providerMap.set(p.id, p);
    }

    // Check default routing channels
    for (const [channel, chain] of Object.entries(routing.default)) {
      for (const provId of chain) {
        const prov = providerMap.get(provId);
        if (!prov) {
          throw new Error(`Default routing for ${channel} references unknown provider: "${provId}"`);
        }
        if (prov.channel !== channel) {
          throw new Error(
            `Default routing mismatch: provider "${provId}" is for channel "${prov.channel}", not "${channel}"`,
          );
        }
      }
    }

    // Check application bindings
    for (const app of applications) {
      for (const [channel, chain] of Object.entries(app.bindings)) {
        if (!chain) continue;
        for (const provId of chain) {
          const prov = providerMap.get(provId);
          if (!prov) {
            throw new Error(
              `Application "${app.id}" binding references unknown provider: "${provId}"`,
            );
          }
          if (prov.channel !== channel) {
            throw new Error(
              `Application "${app.id}" binding mismatch: provider "${provId}" is channel "${prov.channel}", not "${channel}"`,
            );
          }
        }
      }
    }
  }

  /**
   * Atomically save an updated configuration file and archive a versioned backup.
   */
  public loadDltConfig(env = process.env): DltConfigModel {
    const raw = this.readYamlFile('dlt.yaml') ?? { dlt: {} };
    const interpolated = interpolateEnvVars(raw, env);
    return DltFileSchema.parse(interpolated).dlt;
  }

  public loadTemplates(env = process.env): MessageTemplateModel[] {
    const raw = this.readYamlFile('templates.yaml') ?? { templates: [] };
    const interpolated = interpolateEnvVars(raw, env);
    return TemplatesFileSchema.parse(interpolated).templates;
  }

  public loadSuppressions(env = process.env): SuppressionsFileModel['suppressions'] {
    const raw = this.readYamlFile('suppressions.yaml') ?? { suppressions: {} };
    const interpolated = interpolateEnvVars(raw, env);
    return SuppressionsFileSchema.parse(interpolated).suppressions;
  }

  public loadAlerts(env = process.env): AlertsFileModel['alerts'] {
    const raw = this.readYamlFile('alerts.yaml') ?? { alerts: {} };
    const interpolated = interpolateEnvVars(raw, env);
    return AlertsFileSchema.parse(interpolated).alerts;
  }

  public loadTeam(env = process.env): TeamFileModel['team'] {
    const raw = this.readYamlFile('team.yaml') ?? { team: [] };
    const interpolated = interpolateEnvVars(raw, env);
    return TeamFileSchema.parse(interpolated).team;
  }

  public atomicSaveConfig(filename: string, content: unknown): void {
    if (!fs.existsSync(this.configDir)) {
      fs.mkdirSync(this.configDir, { recursive: true });
    }

    const historyDir = path.join(this.stateDir, 'config-history');
    if (!fs.existsSync(historyDir)) {
      fs.mkdirSync(historyDir, { recursive: true });
    }

    const fullPath = path.join(this.configDir, filename);

    // 1. Create timestamped backup of current file if it exists
    if (fs.existsSync(fullPath)) {
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const backupPath = path.join(historyDir, `${filename}.${timestamp}.bak`);
      fs.copyFileSync(fullPath, backupPath);

      // Keep only newest 10 backups per file
      const backups = fs
        .readdirSync(historyDir)
        .filter((f) => f.startsWith(filename) && f.endsWith('.bak'))
        .sort();
      while (backups.length > 10) {
        const oldBackup = backups.shift();
        if (oldBackup) {
          try {
            fs.unlinkSync(path.join(historyDir, oldBackup));
          } catch {
            // ignore
          }
        }
      }
    }

    // 2. Atomic write using temporary file + fsync + rename
    const tmpPath = path.join(this.configDir, `.${filename}.${Date.now()}.tmp`);
    const yamlString = YAML.stringify(content, { indent: 2 });

    const fd = fs.openSync(tmpPath, 'w', 0o644);
    try {
      fs.writeFileSync(fd, yamlString, 'utf8');
      fs.fsyncSync(fd);
    } finally {
      fs.closeSync(fd);
    }

    fs.renameSync(tmpPath, fullPath);
  }
}

