import { logger } from '../logging.ts';
import type { ConnectionManager } from '../classes/connectionmanager.ts';
import { CachedSecret } from './documents/classes.cached.secret.ts';
import { TTL } from './classes.cached.document.ts';
import type { ISecret } from '../../ts_interfaces/data/secret.ts';

export interface IScanResult {
  connectionsScanned: number;
  secretsFound: number;
  errors: string[];
  durationMs: number;
}

/**
 * Centralized secrets scanning service. Fetches all secrets from all
 * connections and upserts them into the CachedSecret collection.
 */
export class SecretsScanService {
  public lastScanTimestamp: number = 0;
  public lastScanResult: IScanResult | null = null;
  public isScanning: boolean = false;

  private connectionManager: ConnectionManager;

  constructor(connectionManager: ConnectionManager) {
    this.connectionManager = connectionManager;
  }

  /**
   * Upsert a single secret into the cache. If a doc with the same composite ID
   * already exists, update it in place; otherwise insert a new one.
   */
  private async upsertSecret(secret: ISecret): Promise<void> {
    const id = CachedSecret.buildId(secret.connectionId, secret.scope, secret.scopeId, secret.key);
    const existing = await CachedSecret.getInstance({ id });
    if (existing) {
      existing.value = secret.value;
      existing.protected = secret.protected;
      existing.masked = secret.masked;
      existing.environment = secret.environment;
      existing.scopeName = secret.scopeName;
      existing.setTTL(TTL.HOURS_24);
      await existing.save();
    } else {
      const doc = CachedSecret.fromISecret(secret);
      await doc.save();
    }
  }

  /**
   * Save an array of secrets to cache using upsert logic.
   * Best-effort: individual failures are silently ignored.
   */
  async saveSecrets(secrets: ISecret[]): Promise<void> {
    for (const secret of secrets) {
      try {
        await this.upsertSecret(secret);
      } catch {
        // Best-effort caching
      }
    }
  }

  /**
   * Full scan: iterate all connections, fetch all projects+groups,
   * fetch all secrets per entity, upsert CachedSecret docs.
   */
  async fullScan(): Promise<IScanResult> {
    if (this.isScanning) {
      return {
        connectionsScanned: 0,
        secretsFound: 0,
        errors: ['Scan already in progress'],
        durationMs: 0,
      };
    }

    this.isScanning = true;
    const startTime = Date.now();
    const errors: string[] = [];
    let totalSecrets = 0;
    let connectionsScanned = 0;

    try {
      const connections = this.connectionManager.getConnections();
      for (const conn of connections) {
        if (conn.status === 'paused') continue;
        try {
          const provider = this.connectionManager.getProvider(conn.id);
          connectionsScanned++;

          // Scan project secrets
          try {
            const projects = await provider.getProjects();
            for (let i = 0; i < projects.length; i += 5) {
              const batch = projects.slice(i, i + 5);
              const results = await Promise.allSettled(
                batch.map(async (p) => {
                  const secrets = await provider.getProjectSecrets(p.id);
                  return secrets.map((s) => ({
                    ...s,
                    scope: 'project' as const,
                    scopeId: p.id,
                    scopeName: p.fullPath || p.name,
                    connectionId: conn.id,
                  }));
                }),
              );
              for (const result of results) {
                if (result.status === 'fulfilled') {
                  for (const secret of result.value) {
                    try {
                      await this.upsertSecret(secret);
                      totalSecrets++;
                    } catch (err) {
                      errors.push(`Save secret ${secret.key}: ${err}`);
                    }
                  }
                } else {
                  errors.push(`Fetch project secrets: ${result.reason}`);
                }
              }
            }
          } catch (err) {
            errors.push(`Fetch projects for ${conn.id}: ${err}`);
          }

          // Scan group secrets
          try {
            const groups = await provider.getGroups();
            for (let i = 0; i < groups.length; i += 5) {
              const batch = groups.slice(i, i + 5);
              const results = await Promise.allSettled(
                batch.map(async (g) => {
                  const secrets = await provider.getGroupSecrets(g.id);
                  return secrets.map((s) => ({
                    ...s,
                    scope: 'group' as const,
                    scopeId: g.id,
                    scopeName: g.fullPath || g.name,
                    connectionId: conn.id,
                  }));
                }),
              );
              for (const result of results) {
                if (result.status === 'fulfilled') {
                  for (const secret of result.value) {
                    try {
                      await this.upsertSecret(secret);
                      totalSecrets++;
                    } catch (err) {
                      errors.push(`Save secret ${secret.key}: ${err}`);
                    }
                  }
                } else {
                  errors.push(`Fetch group secrets: ${result.reason}`);
                }
              }
            }
          } catch (err) {
            errors.push(`Fetch groups for ${conn.id}: ${err}`);
          }
        } catch (err) {
          errors.push(`Connection ${conn.id}: ${err}`);
        }
      }
    } finally {
      this.isScanning = false;
    }

    const result: IScanResult = {
      connectionsScanned,
      secretsFound: totalSecrets,
      errors,
      durationMs: Date.now() - startTime,
    };

    this.lastScanTimestamp = Date.now();
    this.lastScanResult = result;

    logger.info(
      `Secrets scan complete: ${totalSecrets} secrets from ${connectionsScanned} connections in ${result.durationMs}ms` +
        (errors.length > 0 ? ` (${errors.length} errors)` : ''),
    );

    return result;
  }

  /**
   * Scan a single entity: delete existing cached secrets for that entity,
   * fetch fresh from provider, and save to cache.
   */
  async scanEntity(
    connectionId: string,
    scope: 'project' | 'group',
    scopeId: string,
    scopeName?: string,
  ): Promise<void> {
    try {
      // Delete existing cached secrets for this entity
      const existing = await CachedSecret.getInstances({
        connectionId,
        scope,
        scopeId,
      });
      for (const doc of existing) {
        await doc.delete();
      }

      // Fetch fresh from provider
      const provider = this.connectionManager.getProvider(connectionId);
      const secrets = scope === 'project'
        ? await provider.getProjectSecrets(scopeId)
        : await provider.getGroupSecrets(scopeId);

      // Save to cache
      for (const s of secrets) {
        const doc = CachedSecret.fromISecret({
          ...s,
          scope,
          scopeId,
          scopeName: scopeName || s.scopeName || '',
          connectionId,
        });
        await doc.save();
      }
    } catch (err) {
      logger.error(`scanEntity failed for ${connectionId}/${scope}/${scopeId}: ${err}`);
    }
  }

  /**
   * Get cached secrets matching the filter criteria.
   */
  async getCachedSecrets(filter: {
    connectionId: string;
    scope: 'project' | 'group';
    scopeId?: string;
  }): Promise<ISecret[]> {
    // deno-lint-ignore no-explicit-any
    const query: any = {
      connectionId: filter.connectionId,
      scope: filter.scope,
    };
    if (filter.scopeId) {
      query.scopeId = filter.scopeId;
    }
    const docs = await CachedSecret.getInstances(query);
    // Filter out expired docs
    const now = Date.now();
    return docs
      .filter((d) => d.expiresAt > now)
      .map((d) => d.toISecret());
  }

  /**
   * Check if non-expired cached data exists for the given connection+scope.
   */
  async hasCachedData(connectionId: string, scope: 'project' | 'group'): Promise<boolean> {
    const docs = await CachedSecret.getInstances({
      connectionId,
      scope,
      expiresAt: { $gt: Date.now() },
    });
    return docs.length > 0;
  }
}
