import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SourceProfileDoc, NetworkTargetDoc, RouteDoc } from '../db/index.js';
import type {
  ISourceProfile,
  INetworkTarget,
  IRouteMetadata,
  IRoute,
  IRouteSecurity,
} from '../../ts_interfaces/data/route-management.js';

const MAX_INHERITANCE_DEPTH = 5;

export class ReferenceResolver {
  private profiles = new Map<string, ISourceProfile>();
  private targets = new Map<string, INetworkTarget>();

  // =========================================================================
  // Lifecycle
  // =========================================================================

  public async initialize(): Promise<void> {
    await this.loadProfiles();
    await this.loadTargets();
  }

  // =========================================================================
  // Profile CRUD
  // =========================================================================

  public async createProfile(data: {
    name: string;
    description?: string;
    security: IRouteSecurity;
    extendsProfiles?: string[];
    createdBy: string;
  }): Promise<string> {
    const id = plugins.uuid.v4();
    const now = Date.now();

    const profile: ISourceProfile = {
      id,
      name: data.name,
      description: data.description,
      security: data.security,
      extendsProfiles: data.extendsProfiles,
      createdAt: now,
      updatedAt: now,
      createdBy: data.createdBy,
    };

    this.profiles.set(id, profile);
    await this.persistProfile(profile);
    logger.log('info', `Created source profile '${profile.name}' (${id})`);
    return id;
  }

  public async updateProfile(
    id: string,
    patch: Partial<Omit<ISourceProfile, 'id' | 'createdAt' | 'createdBy'>>,
  ): Promise<{ affectedRouteIds: string[] }> {
    const profile = this.profiles.get(id);
    if (!profile) {
      throw new Error(`Source profile '${id}' not found`);
    }

    if (patch.name !== undefined) profile.name = patch.name;
    if (patch.description !== undefined) profile.description = patch.description;
    if (patch.security !== undefined) profile.security = patch.security;
    if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles;
    profile.updatedAt = Date.now();

    await this.persistProfile(profile);
    logger.log('info', `Updated source profile '${profile.name}' (${id})`);

    // Find routes referencing this profile
    const affectedRouteIds = await this.findRoutesByProfileRef(id);
    return { affectedRouteIds };
  }

  public async deleteProfile(
    id: string,
    force: boolean,
    storedRoutes?: Map<string, IRoute>,
  ): Promise<{ success: boolean; message?: string }> {
    const profile = this.profiles.get(id);
    if (!profile) {
      return { success: false, message: `Source profile '${id}' not found` };
    }

    // Check usage
    const affectedIds = storedRoutes
      ? this.findRoutesByProfileRefSync(id, storedRoutes)
      : await this.findRoutesByProfileRef(id);

    if (affectedIds.length > 0 && !force) {
      return {
        success: false,
        message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
      };
    }

    // Delete from DB
    const doc = await SourceProfileDoc.findById(id);
    if (doc) await doc.delete();
    this.profiles.delete(id);

    // If force-deleting with referencing routes, clear refs but keep resolved values
    if (affectedIds.length > 0) {
      await this.clearProfileRefsOnRoutes(affectedIds);
      logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
    } else {
      logger.log('info', `Deleted source profile '${profile.name}' (${id})`);
    }

    return { success: true };
  }

  public getProfile(id: string): ISourceProfile | undefined {
    return this.profiles.get(id);
  }

  public getProfileByName(name: string): ISourceProfile | undefined {
    for (const profile of this.profiles.values()) {
      if (profile.name === name) return profile;
    }
    return undefined;
  }

  public listProfiles(): ISourceProfile[] {
    return [...this.profiles.values()];
  }

  public getProfileUsage(storedRoutes: Map<string, IRoute>): Map<string, Array<{ id: string; routeName: string }>> {
    const usage = new Map<string, Array<{ id: string; routeName: string }>>();
    for (const profile of this.profiles.values()) {
      usage.set(profile.id, []);
    }
    for (const [routeId, stored] of storedRoutes) {
      const ref = stored.metadata?.sourceProfileRef;
      if (ref && usage.has(ref)) {
        usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
      }
    }
    return usage;
  }

  public getProfileUsageForId(
    profileId: string,
    storedRoutes: Map<string, IRoute>,
  ): Array<{ id: string; routeName: string }> {
    const routes: Array<{ id: string; routeName: string }> = [];
    for (const [routeId, stored] of storedRoutes) {
      if (stored.metadata?.sourceProfileRef === profileId) {
        routes.push({ id: routeId, routeName: stored.route.name || routeId });
      }
    }
    return routes;
  }

  // =========================================================================
  // Target CRUD
  // =========================================================================

  public async createTarget(data: {
    name: string;
    description?: string;
    host: string | string[];
    port: number;
    createdBy: string;
  }): Promise<string> {
    const id = plugins.uuid.v4();
    const now = Date.now();

    const target: INetworkTarget = {
      id,
      name: data.name,
      description: data.description,
      host: data.host,
      port: data.port,
      createdAt: now,
      updatedAt: now,
      createdBy: data.createdBy,
    };

    this.targets.set(id, target);
    await this.persistTarget(target);
    logger.log('info', `Created network target '${target.name}' (${id})`);
    return id;
  }

  public async updateTarget(
    id: string,
    patch: Partial<Omit<INetworkTarget, 'id' | 'createdAt' | 'createdBy'>>,
  ): Promise<{ affectedRouteIds: string[] }> {
    const target = this.targets.get(id);
    if (!target) {
      throw new Error(`Network target '${id}' not found`);
    }

    if (patch.name !== undefined) target.name = patch.name;
    if (patch.description !== undefined) target.description = patch.description;
    if (patch.host !== undefined) target.host = patch.host;
    if (patch.port !== undefined) target.port = patch.port;
    target.updatedAt = Date.now();

    await this.persistTarget(target);
    logger.log('info', `Updated network target '${target.name}' (${id})`);

    const affectedRouteIds = await this.findRoutesByTargetRef(id);
    return { affectedRouteIds };
  }

  public async deleteTarget(
    id: string,
    force: boolean,
    storedRoutes?: Map<string, IRoute>,
  ): Promise<{ success: boolean; message?: string }> {
    const target = this.targets.get(id);
    if (!target) {
      return { success: false, message: `Network target '${id}' not found` };
    }

    const affectedIds = storedRoutes
      ? this.findRoutesByTargetRefSync(id, storedRoutes)
      : await this.findRoutesByTargetRef(id);

    if (affectedIds.length > 0 && !force) {
      return {
        success: false,
        message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
      };
    }

    const doc = await NetworkTargetDoc.findById(id);
    if (doc) await doc.delete();
    this.targets.delete(id);

    if (affectedIds.length > 0) {
      await this.clearTargetRefsOnRoutes(affectedIds);
      logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`);
    } else {
      logger.log('info', `Deleted network target '${target.name}' (${id})`);
    }

    return { success: true };
  }

  public getTarget(id: string): INetworkTarget | undefined {
    return this.targets.get(id);
  }

  public getTargetByName(name: string): INetworkTarget | undefined {
    for (const target of this.targets.values()) {
      if (target.name === name) return target;
    }
    return undefined;
  }

  public listTargets(): INetworkTarget[] {
    return [...this.targets.values()];
  }

  public getTargetUsageForId(
    targetId: string,
    storedRoutes: Map<string, IRoute>,
  ): Array<{ id: string; routeName: string }> {
    const routes: Array<{ id: string; routeName: string }> = [];
    for (const [routeId, stored] of storedRoutes) {
      if (stored.metadata?.networkTargetRef === targetId) {
        routes.push({ id: routeId, routeName: stored.route.name || routeId });
      }
    }
    return routes;
  }

  // =========================================================================
  // Resolution
  // =========================================================================

  /**
   * Resolve references for a single route.
   * Materializes source profile and/or network target into the route's fields.
   * Returns the resolved route and updated metadata.
   */
  public resolveRoute(
    route: plugins.smartproxy.IRouteConfig,
    metadata?: IRouteMetadata,
  ): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
    const resolvedMetadata: IRouteMetadata = { ...metadata };

    if (resolvedMetadata.sourceProfileRef) {
      const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
      if (resolvedSecurity) {
        const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
        // Merge: profile provides base, route's inline values override
        route = {
          ...route,
          security: this.mergeSecurityFields(resolvedSecurity, route.security),
        };
        resolvedMetadata.sourceProfileName = profile?.name;
        resolvedMetadata.lastResolvedAt = Date.now();
      } else {
        logger.log('warn', `Source profile '${resolvedMetadata.sourceProfileRef}' not found during resolution`);
      }
    }

    if (resolvedMetadata.networkTargetRef) {
      const target = this.targets.get(resolvedMetadata.networkTargetRef);
      if (target) {
        const hosts = Array.isArray(target.host) ? target.host : [target.host];
        route = {
          ...route,
          action: {
            ...route.action,
            targets: hosts.map((h) => ({
              host: h,
              port: target.port,
            })),
          },
        };
        resolvedMetadata.networkTargetName = target.name;
        resolvedMetadata.lastResolvedAt = Date.now();
      } else {
        logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`);
      }
    }

    return { route, metadata: resolvedMetadata };
  }

  // =========================================================================
  // Reference lookup helpers
  // =========================================================================

  public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
    const docs = await RouteDoc.findAll();
    return docs
      .filter((doc) => doc.metadata?.sourceProfileRef === profileId)
      .map((doc) => doc.id);
  }

  public async findRoutesByTargetRef(targetId: string): Promise<string[]> {
    const docs = await RouteDoc.findAll();
    return docs
      .filter((doc) => doc.metadata?.networkTargetRef === targetId)
      .map((doc) => doc.id);
  }

  public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IRoute>): string[] {
    const ids: string[] = [];
    for (const [routeId, stored] of storedRoutes) {
      if (stored.metadata?.sourceProfileRef === profileId) {
        ids.push(routeId);
      }
    }
    return ids;
  }

  public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map<string, IRoute>): string[] {
    const ids: string[] = [];
    for (const [routeId, stored] of storedRoutes) {
      if (stored.metadata?.networkTargetRef === targetId) {
        ids.push(routeId);
      }
    }
    return ids;
  }

  // =========================================================================
  // Private: source profile resolution with inheritance
  // =========================================================================

  private resolveSourceProfile(
    profileId: string,
    visited: Set<string> = new Set(),
    depth: number = 0,
  ): IRouteSecurity | null {
    if (depth > MAX_INHERITANCE_DEPTH) {
      logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`);
      return null;
    }

    if (visited.has(profileId)) {
      logger.log('warn', `Circular inheritance detected for profile '${profileId}'`);
      return null;
    }

    const profile = this.profiles.get(profileId);
    if (!profile) return null;

    visited.add(profileId);

    // Start with an empty base
    let baseSecurity: IRouteSecurity = {};

    // Resolve parent profiles first (top-down, later overrides earlier)
    if (profile.extendsProfiles?.length) {
      for (const parentId of profile.extendsProfiles) {
        const parentSecurity = this.resolveSourceProfile(parentId, new Set(visited), depth + 1);
        if (parentSecurity) {
          baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
        }
      }
    }

    // Apply this profile's security on top
    return this.mergeSecurityFields(baseSecurity, profile.security);
  }

  /**
   * Merge two IRouteSecurity objects.
   * `override` values take precedence over `base` values.
   * For ipAllowList/ipBlockList: union arrays and deduplicate.
   * For scalar/object fields: override wins if present.
   */
  private mergeSecurityFields(
    base: IRouteSecurity | undefined,
    override: IRouteSecurity | undefined,
  ): IRouteSecurity {
    if (!base && !override) return {};
    if (!base) return { ...override };
    if (!override) return { ...base };

    const merged: IRouteSecurity = { ...base };

    // IP lists: union
    if (override.ipAllowList || base.ipAllowList) {
      merged.ipAllowList = [...new Set([
        ...(base.ipAllowList || []),
        ...(override.ipAllowList || []),
      ])];
    }

    if (override.ipBlockList || base.ipBlockList) {
      merged.ipBlockList = [...new Set([
        ...(base.ipBlockList || []),
        ...(override.ipBlockList || []),
      ])];
    }

    // Scalar/object fields: override wins
    if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections;
    if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit;
    if (override.authentication !== undefined) merged.authentication = override.authentication;
    if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
    if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;

    return merged;
  }

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

  private async loadProfiles(): Promise<void> {
    const docs = await SourceProfileDoc.findAll();
    for (const doc of docs) {
      if (doc.id) {
        this.profiles.set(doc.id, {
          id: doc.id,
          name: doc.name,
          description: doc.description,
          security: doc.security,
          extendsProfiles: doc.extendsProfiles,
          createdAt: doc.createdAt,
          updatedAt: doc.updatedAt,
          createdBy: doc.createdBy,
        });
      }
    }
    if (this.profiles.size > 0) {
      logger.log('info', `Loaded ${this.profiles.size} source profile(s) from storage`);
    }
  }

  private async loadTargets(): Promise<void> {
    const docs = await NetworkTargetDoc.findAll();
    for (const doc of docs) {
      if (doc.id) {
        this.targets.set(doc.id, {
          id: doc.id,
          name: doc.name,
          description: doc.description,
          host: doc.host,
          port: doc.port,
          createdAt: doc.createdAt,
          updatedAt: doc.updatedAt,
          createdBy: doc.createdBy,
        });
      }
    }
    if (this.targets.size > 0) {
      logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`);
    }
  }

  private async persistProfile(profile: ISourceProfile): Promise<void> {
    const existingDoc = await SourceProfileDoc.findById(profile.id);
    if (existingDoc) {
      existingDoc.name = profile.name;
      existingDoc.description = profile.description;
      existingDoc.security = profile.security;
      existingDoc.extendsProfiles = profile.extendsProfiles;
      existingDoc.updatedAt = profile.updatedAt;
      await existingDoc.save();
    } else {
      const doc = new SourceProfileDoc();
      doc.id = profile.id;
      doc.name = profile.name;
      doc.description = profile.description;
      doc.security = profile.security;
      doc.extendsProfiles = profile.extendsProfiles;
      doc.createdAt = profile.createdAt;
      doc.updatedAt = profile.updatedAt;
      doc.createdBy = profile.createdBy;
      await doc.save();
    }
  }

  private async persistTarget(target: INetworkTarget): Promise<void> {
    const existingDoc = await NetworkTargetDoc.findById(target.id);
    if (existingDoc) {
      existingDoc.name = target.name;
      existingDoc.description = target.description;
      existingDoc.host = target.host;
      existingDoc.port = target.port;
      existingDoc.updatedAt = target.updatedAt;
      await existingDoc.save();
    } else {
      const doc = new NetworkTargetDoc();
      doc.id = target.id;
      doc.name = target.name;
      doc.description = target.description;
      doc.host = target.host;
      doc.port = target.port;
      doc.createdAt = target.createdAt;
      doc.updatedAt = target.updatedAt;
      doc.createdBy = target.createdBy;
      await doc.save();
    }
  }

  // =========================================================================
  // Private: ref cleanup on force-delete
  // =========================================================================

  private async clearProfileRefsOnRoutes(routeIds: string[]): Promise<void> {
    for (const routeId of routeIds) {
      const doc = await RouteDoc.findById(routeId);
      if (doc?.metadata) {
        doc.metadata = {
          ...doc.metadata,
          sourceProfileRef: undefined,
          sourceProfileName: undefined,
        };
        doc.updatedAt = Date.now();
        await doc.save();
      }
    }
  }

  private async clearTargetRefsOnRoutes(routeIds: string[]): Promise<void> {
    for (const routeId of routeIds) {
      const doc = await RouteDoc.findById(routeId);
      if (doc?.metadata) {
        doc.metadata = {
          ...doc.metadata,
          networkTargetRef: undefined,
          networkTargetName: undefined,
        };
        doc.updatedAt = Date.now();
        await doc.save();
      }
    }
  }
}
