import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { StoredRouteDoc, RouteOverrideDoc } from '../db/index.js';
import type {
  IStoredRoute,
  IRouteOverride,
  IMergedRoute,
  IRouteWarning,
  IRouteMetadata,
} from '../../ts_interfaces/data/route-management.js';
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
import type { ReferenceResolver } from './classes.reference-resolver.js';

export class RouteConfigManager {
  private storedRoutes = new Map<string, IStoredRoute>();
  private overrides = new Map<string, IRouteOverride>();
  private warnings: IRouteWarning[] = [];

  constructor(
    private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
    private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
    private getHttp3Config?: () => IHttp3Config | undefined,
    private getVpnAllowList?: (tags?: string[]) => string[],
    private referenceResolver?: ReferenceResolver,
    private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
  ) {}

  /** Expose stored routes map for reference resolution lookups. */
  public getStoredRoutes(): Map<string, IStoredRoute> {
    return this.storedRoutes;
  }

  /**
   * Load persisted routes and overrides, compute warnings, apply to SmartProxy.
   */
  public async initialize(): Promise<void> {
    await this.loadStoredRoutes();
    await this.loadOverrides();
    this.computeWarnings();
    this.logWarnings();
    await this.applyRoutes();
  }

  // =========================================================================
  // Merged view
  // =========================================================================

  public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
    const merged: IMergedRoute[] = [];

    // Hardcoded routes
    for (const route of this.getHardcodedRoutes()) {
      const name = route.name || '';
      const override = this.overrides.get(name);
      merged.push({
        route,
        source: 'hardcoded',
        enabled: override ? override.enabled : true,
        overridden: !!override,
      });
    }

    // Programmatic routes
    for (const stored of this.storedRoutes.values()) {
      merged.push({
        route: stored.route,
        source: 'programmatic',
        enabled: stored.enabled,
        overridden: false,
        storedRouteId: stored.id,
        createdAt: stored.createdAt,
        updatedAt: stored.updatedAt,
        metadata: stored.metadata,
      });
    }

    return { routes: merged, warnings: [...this.warnings] };
  }

  // =========================================================================
  // Programmatic route CRUD
  // =========================================================================

  public async createRoute(
    route: plugins.smartproxy.IRouteConfig,
    createdBy: string,
    enabled = true,
    metadata?: IRouteMetadata,
  ): Promise<string> {
    const id = plugins.uuid.v4();
    const now = Date.now();

    // Ensure route has a name
    if (!route.name) {
      route.name = `programmatic-${id.slice(0, 8)}`;
    }

    // Resolve references if metadata has refs and resolver is available
    let resolvedMetadata = metadata;
    if (metadata && this.referenceResolver) {
      const resolved = this.referenceResolver.resolveRoute(route, metadata);
      route = resolved.route;
      resolvedMetadata = resolved.metadata;
    }

    const stored: IStoredRoute = {
      id,
      route,
      enabled,
      createdAt: now,
      updatedAt: now,
      createdBy,
      metadata: resolvedMetadata,
    };

    this.storedRoutes.set(id, stored);
    await this.persistRoute(stored);
    await this.applyRoutes();
    return id;
  }

  public async updateRoute(
    id: string,
    patch: {
      route?: Partial<plugins.smartproxy.IRouteConfig>;
      enabled?: boolean;
      metadata?: Partial<IRouteMetadata>;
    },
  ): Promise<boolean> {
    const stored = this.storedRoutes.get(id);
    if (!stored) return false;

    if (patch.route) {
      stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig;
    }
    if (patch.enabled !== undefined) {
      stored.enabled = patch.enabled;
    }
    if (patch.metadata !== undefined) {
      stored.metadata = { ...stored.metadata, ...patch.metadata };
    }

    // Re-resolve if metadata refs exist and resolver is available
    if (stored.metadata && this.referenceResolver) {
      const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
      stored.route = resolved.route;
      stored.metadata = resolved.metadata;
    }

    stored.updatedAt = Date.now();

    await this.persistRoute(stored);
    await this.applyRoutes();
    return true;
  }

  public async deleteRoute(id: string): Promise<boolean> {
    if (!this.storedRoutes.has(id)) return false;
    this.storedRoutes.delete(id);
    const doc = await StoredRouteDoc.findById(id);
    if (doc) await doc.delete();
    await this.applyRoutes();
    return true;
  }

  public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
    return this.updateRoute(id, { enabled });
  }

  // =========================================================================
  // Hardcoded route overrides
  // =========================================================================

  public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise<void> {
    const override: IRouteOverride = {
      routeName,
      enabled,
      updatedAt: Date.now(),
      updatedBy,
    };
    this.overrides.set(routeName, override);
    const existingDoc = await RouteOverrideDoc.findByRouteName(routeName);
    if (existingDoc) {
      existingDoc.enabled = override.enabled;
      existingDoc.updatedAt = override.updatedAt;
      existingDoc.updatedBy = override.updatedBy;
      await existingDoc.save();
    } else {
      const doc = new RouteOverrideDoc();
      doc.routeName = override.routeName;
      doc.enabled = override.enabled;
      doc.updatedAt = override.updatedAt;
      doc.updatedBy = override.updatedBy;
      await doc.save();
    }
    this.computeWarnings();
    await this.applyRoutes();
  }

  public async removeOverride(routeName: string): Promise<boolean> {
    if (!this.overrides.has(routeName)) return false;
    this.overrides.delete(routeName);
    const doc = await RouteOverrideDoc.findByRouteName(routeName);
    if (doc) await doc.delete();
    this.computeWarnings();
    await this.applyRoutes();
    return true;
  }

  // =========================================================================
  // Private: persistence
  // =========================================================================

  private async loadStoredRoutes(): Promise<void> {
    const docs = await StoredRouteDoc.findAll();
    for (const doc of docs) {
      if (doc.id) {
        this.storedRoutes.set(doc.id, {
          id: doc.id,
          route: doc.route,
          enabled: doc.enabled,
          createdAt: doc.createdAt,
          updatedAt: doc.updatedAt,
          createdBy: doc.createdBy,
          metadata: doc.metadata,
        });
      }
    }
    if (this.storedRoutes.size > 0) {
      logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`);
    }
  }

  private async loadOverrides(): Promise<void> {
    const docs = await RouteOverrideDoc.findAll();
    for (const doc of docs) {
      if (doc.routeName) {
        this.overrides.set(doc.routeName, {
          routeName: doc.routeName,
          enabled: doc.enabled,
          updatedAt: doc.updatedAt,
          updatedBy: doc.updatedBy,
        });
      }
    }
    if (this.overrides.size > 0) {
      logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`);
    }
  }

  private async persistRoute(stored: IStoredRoute): Promise<void> {
    const existingDoc = await StoredRouteDoc.findById(stored.id);
    if (existingDoc) {
      existingDoc.route = stored.route;
      existingDoc.enabled = stored.enabled;
      existingDoc.updatedAt = stored.updatedAt;
      existingDoc.createdBy = stored.createdBy;
      existingDoc.metadata = stored.metadata;
      await existingDoc.save();
    } else {
      const doc = new StoredRouteDoc();
      doc.id = stored.id;
      doc.route = stored.route;
      doc.enabled = stored.enabled;
      doc.createdAt = stored.createdAt;
      doc.updatedAt = stored.updatedAt;
      doc.createdBy = stored.createdBy;
      doc.metadata = stored.metadata;
      await doc.save();
    }
  }

  // =========================================================================
  // Private: warnings
  // =========================================================================

  private computeWarnings(): void {
    this.warnings = [];
    const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || ''));

    // Check overrides
    for (const [routeName, override] of this.overrides) {
      if (!hardcodedNames.has(routeName)) {
        this.warnings.push({
          type: 'orphaned-override',
          routeName,
          message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`,
        });
      } else if (!override.enabled) {
        this.warnings.push({
          type: 'disabled-hardcoded',
          routeName,
          message: `Route '${routeName}' is disabled via API override`,
        });
      }
    }

    // Check disabled programmatic routes
    for (const stored of this.storedRoutes.values()) {
      if (!stored.enabled) {
        const name = stored.route.name || stored.id;
        this.warnings.push({
          type: 'disabled-programmatic',
          routeName: name,
          message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`,
        });
      }
    }
  }

  private logWarnings(): void {
    for (const w of this.warnings) {
      logger.log('warn', w.message);
    }
  }

  // =========================================================================
  // Re-resolve routes after profile/target changes
  // =========================================================================

  /**
   * Re-resolve specific routes by ID (after a profile or target is updated).
   * Persists each route and calls applyRoutes() once at the end.
   */
  public async reResolveRoutes(routeIds: string[]): Promise<void> {
    if (!this.referenceResolver || routeIds.length === 0) return;

    for (const routeId of routeIds) {
      const stored = this.storedRoutes.get(routeId);
      if (!stored?.metadata) continue;

      const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
      stored.route = resolved.route;
      stored.metadata = resolved.metadata;
      stored.updatedAt = Date.now();
      await this.persistRoute(stored);
    }

    await this.applyRoutes();
    logger.log('info', `Re-resolved ${routeIds.length} route(s) after profile/target change`);
  }

  // =========================================================================
  // Private: apply merged routes to SmartProxy
  // =========================================================================

  public async applyRoutes(): Promise<void> {
    const smartProxy = this.getSmartProxy();
    if (!smartProxy) return;

    const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];

    const http3Config = this.getHttp3Config?.();
    const vpnAllowList = this.getVpnAllowList;

    // Helper: inject VPN security into a route if vpn.enabled is set
    const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => {
      if (!vpnAllowList) return route;
      const dcRoute = route as IDcRouterRouteConfig;
      if (!dcRoute.vpn?.enabled) return route;
      const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
      const mandatory = dcRoute.vpn.mandatory === true; // defaults to false
      return {
        ...route,
        security: {
          ...route.security,
          ipAllowList: mandatory
            ? allowList
            : [...(route.security?.ipAllowList || []), ...allowList],
        },
      };
    };

    // Add enabled hardcoded routes (respecting overrides, with fresh VPN injection)
    for (const route of this.getHardcodedRoutes()) {
      const name = route.name || '';
      const override = this.overrides.get(name);
      if (override && !override.enabled) {
        continue; // Skip disabled hardcoded route
      }
      enabledRoutes.push(injectVpn(route));
    }

    // Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
    for (const stored of this.storedRoutes.values()) {
      if (stored.enabled) {
        let route = stored.route;
        if (http3Config && http3Config.enabled !== false) {
          route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
        }
        enabledRoutes.push(injectVpn(route));
      }
    }

    await smartProxy.updateRoutes(enabledRoutes);

    // Notify listeners (e.g. RemoteIngressManager) of the merged route set
    if (this.onRoutesApplied) {
      this.onRoutesApplied(enabledRoutes);
    }

    logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
  }
}
