import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service";
import * as jsforce from "jsforce";
import * as jwt from "jsonwebtoken";
import * as fs from "fs/promises";
import * as path from "path";

export interface SalesforceSObjectApi {
  create: (data: Record<string, unknown>) => Promise<{ id?: string }>;
  update?: (data: Record<string, unknown> & { Id: string }) => Promise<unknown>;
}

@Injectable()
export class SalesforceConnection {
  private connection: jsforce.Connection;
  private tokenExpiresAt: number | null = null;
  private tokenIssuedAt: number | null = null;
  private connectPromise: Promise<void> | null = null;

  constructor(
    private configService: ConfigService,
    private readonly requestQueue: SalesforceRequestQueueService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.connection = new jsforce.Connection({
      loginUrl: this.configService.get<string>("SF_LOGIN_URL") || "https://login.salesforce.com",
    });
  }

  /**
   * Expose jsforce connection for advanced features
   * Consumers must ensure connect() has been called and isConnected() is true.
   */
  getConnection(): jsforce.Connection {
    return this.connection;
  }

  /** Get the current OAuth access token (after connect()). */
  getAccessToken(): string | undefined {
    return this.connection?.accessToken as string | undefined;
  }

  /** Get the current instance URL (after connect()). */
  getInstanceUrl(): string | undefined {
    return this.connection?.instanceUrl as string | undefined;
  }

  async connect(force = false): Promise<void> {
    if (this.connectPromise && !force) {
      return this.connectPromise;
    }

    this.connectPromise = this.performConnect();
    try {
      await this.connectPromise;
    } finally {
      this.connectPromise = null;
    }
  }

  private async performConnect(): Promise<void> {
    const nodeEnv =
      this.configService.get<string>("NODE_ENV") || process.env.NODE_ENV || "development";
    const isProd = nodeEnv === "production";
    const authTimeout = this.configService.get<number>("SF_AUTH_TIMEOUT_MS", 30000);
    const startTime = Date.now();

    this.logger.debug("Starting Salesforce authentication", {
      authTimeout,
      nodeEnv,
    });

    try {
      const username = this.configService.get<string>("SF_USERNAME");
      const clientId = this.configService.get<string>("SF_CLIENT_ID");
      const privateKeyPath = this.configService.get<string>("SF_PRIVATE_KEY_PATH");
      const audience =
        this.configService.get<string>("SF_LOGIN_URL") || "https://login.salesforce.com";

      // Gracefully skip connection if not configured for local/dev environments
      if (!username || !clientId || !privateKeyPath) {
        const devMessage =
          "Missing required Salesforce configuration. Please check SF_USERNAME, SF_CLIENT_ID, and SF_PRIVATE_KEY_PATH environment variables.";
        throw new Error(isProd ? "Salesforce configuration is missing" : devMessage);
      }

      // Resolve private key and enforce allowed secrets directories
      // Supports project-root ./secrets, apps/bff ./secrets, and container /app/secrets
      const getProjectRoot = () => {
        const cwd = process.cwd();
        const norm = path.normalize(cwd);
        const bffSuffix = path.normalize(path.join("apps", "bff"));
        if (norm.endsWith(bffSuffix)) {
          return path.resolve(cwd, "../..");
        }
        return cwd;
      };

      const isAbsolute = path.isAbsolute(privateKeyPath);
      const resolvedKeyPath = isAbsolute
        ? privateKeyPath
        : path.resolve(getProjectRoot(), privateKeyPath);

      const allowedBases = [
        path.resolve(getProjectRoot(), "secrets"),
        path.resolve(process.cwd(), "secrets"),
        "/app/secrets",
      ].map(p => path.normalize(p) + path.sep);

      const normalizedKeyPath = path.normalize(resolvedKeyPath);
      const isUnderAllowedBase = allowedBases.some(base =>
        (normalizedKeyPath + path.sep).startsWith(base)
      );
      if (!isUnderAllowedBase) {
        const devMsg = `Salesforce private key must be under one of the allowed secrets directories: ${allowedBases
          .map(b => b.replace(/\\$/, ""))
          .join(", ")}. Got: ${normalizedKeyPath}`;
        throw new Error(isProd ? "Invalid Salesforce private key path" : devMsg);
      }

      try {
        await fs.access(resolvedKeyPath);
      } catch {
        const devMsg = `Salesforce private key file not found at: ${resolvedKeyPath}. Ensure the file exists and has proper permissions (chmod 600).`;
        throw new Error(isProd ? "Salesforce private key not found" : devMsg);
      }

      // Load private key
      const privateKey = await fs.readFile(resolvedKeyPath, "utf8");

      // Validate private key format
      if (!privateKey.includes("BEGIN PRIVATE KEY") || privateKey.includes("[PLACEHOLDER")) {
        const devMsg =
          "Salesforce private key appears to be invalid or still contains placeholder content. Please replace with your actual private key.";
        throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
      }

      // Create JWT assertion
      const payload = {
        iss: clientId,
        sub: username,
        aud: audience,
        exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
      };

      const assertion = jwt.sign(payload, privateKey, { algorithm: "RS256" });

      // Get access token with timeout
      const tokenUrl = `${audience}/services/oauth2/token`;

      this.logger.debug("Requesting Salesforce access token", {
        tokenUrl: isProd ? "[REDACTED]" : tokenUrl,
        clientId: isProd ? "[REDACTED]" : clientId,
        username: isProd ? "[REDACTED]" : username,
      });

      const controller = new AbortController();
      const timeoutId = setTimeout(() => {
        controller.abort();
      }, authTimeout);

      let res: Response;
      try {
        res = await fetch(tokenUrl, {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
            assertion,
          }),
          signal: controller.signal,
        });
      } catch (error) {
        if (error instanceof Error && error.name === "AbortError") {
          const authDuration = Date.now() - startTime;
          this.logger.error("Salesforce authentication timeout", {
            authTimeout,
            authDuration,
            tokenUrl: isProd ? "[REDACTED]" : tokenUrl,
          });
          throw new Error(
            isProd
              ? "Salesforce authentication timeout"
              : `Authentication timeout after ${authTimeout}ms`
          );
        }
        throw error;
      } finally {
        clearTimeout(timeoutId);
      }

      if (!res.ok) {
        const errorText = await res.text();
        const logMsg = `Token request failed: ${res.status}` + (isProd ? "" : ` - ${errorText}`);
        this.logger.error(logMsg);
        throw new Error(
          isProd
            ? "Salesforce authentication failed"
            : `Token request failed: ${res.status} - ${errorText}`
        );
      }

      const tokenResponse = (await res.json()) as { access_token: string; instance_url: string };

      this.connection.accessToken = tokenResponse.access_token;
      this.connection.instanceUrl = tokenResponse.instance_url;

      const tokenTtlMs = this.getTokenTtl();
      const issuedAt = Date.now();
      this.tokenIssuedAt = issuedAt;
      this.tokenExpiresAt = issuedAt + tokenTtlMs;

      const authDuration = issuedAt - startTime;
      this.logger.log("✅ Salesforce connection established", {
        authDuration,
        tokenTtlMs,
        expiresAt: new Date(this.tokenExpiresAt).toISOString(),
        instanceUrl: isProd ? "[REDACTED]" : this.connection.instanceUrl,
      });
    } catch (error) {
      const authDuration = Date.now() - startTime;
      const message = getErrorMessage(error);

      if (isProd) {
        this.logger.error("Failed to connect to Salesforce", {
          authDuration,
          errorType: error instanceof Error ? error.constructor.name : "Unknown",
        });
      } else {
        this.logger.error(`Failed to connect to Salesforce: ${message}`, {
          authDuration,
          errorType: error instanceof Error ? error.constructor.name : "Unknown",
          errorMessage: message,
        });
      }
      throw error;
    }
  }

  // Expose connection methods with automatic re-authentication
  async query(soql: string, options: { label?: string } = {}): Promise<unknown> {
    const priority = this.getQueryPriority(soql);
    const isLongRunning = this.isLongRunningQuery(soql);
    const label = options.label ?? this.deriveQueryLabel(soql);

    return this.requestQueue.execute(
      async () => {
        await this.ensureConnected();
        try {
          return await this.connection.query(soql);
        } catch (error: unknown) {
          if (this.isSessionExpiredError(error)) {
            const reAuthStartTime = Date.now();
            this.logger.warn("Salesforce session expired, attempting to re-authenticate", {
              originalError: getErrorMessage(error),
              tokenIssuedAt: this.tokenIssuedAt ? new Date(this.tokenIssuedAt).toISOString() : null,
              tokenExpiresAt: this.tokenExpiresAt
                ? new Date(this.tokenExpiresAt).toISOString()
                : null,
            });

            try {
              await this.connect(true);

              const reAuthDuration = Date.now() - reAuthStartTime;
              this.logger.debug("Retrying query after re-authentication", {
                reAuthDuration,
              });

              return await this.connection.query(soql);
            } catch (retryError) {
              const reAuthDuration = Date.now() - reAuthStartTime;
              this.logger.error("Failed to re-authenticate or retry query", {
                originalError: getErrorMessage(error),
                retryError: getErrorMessage(retryError),
                reAuthDuration,
              });
              throw retryError;
            }
          }

          throw error;
        }
      },
      { priority, isLongRunning, label }
    );
  }

  private isSessionExpiredError(error: unknown): boolean {
    // Check for various session expiration indicators
    const errorMessage = getErrorMessage(error).toLowerCase();
    const errorCode =
      (error as { errorCode?: string; name?: string })?.errorCode ||
      (error as { errorCode?: string; name?: string })?.name ||
      "";

    return (
      errorCode === "INVALID_SESSION_ID" ||
      errorMessage.includes("session expired") ||
      errorMessage.includes("invalid session") ||
      errorMessage.includes("invalid_session_id") ||
      ((error as { status?: number })?.status === 401 && errorMessage.includes("unauthorized"))
    );
  }

  sobject(type: string): SalesforceSObjectApi {
    const originalSObject = this.connection.sobject(type);

    // Return a wrapper that handles session expiration for SObject operations
    return {
      create: async (data: object) => {
        return this.requestQueue.executeHighPriority(
          async () => {
            try {
              await this.ensureConnected();
              return await originalSObject.create(data);
            } catch (error: unknown) {
              if (this.isSessionExpiredError(error)) {
                const reAuthStartTime = Date.now();
                this.logger.warn(
                  "Salesforce session expired during SObject create, attempting to re-authenticate",
                  {
                    sobjectType: type,
                    originalError: getErrorMessage(error),
                  }
                );

                try {
                  await this.connect(true);
                  const reAuthDuration = Date.now() - reAuthStartTime;
                  this.logger.debug("Retrying SObject create after re-authentication", {
                    sobjectType: type,
                    reAuthDuration,
                  });

                  const newSObject = this.connection.sobject(type);
                  return await newSObject.create(data);
                } catch (retryError) {
                  const reAuthDuration = Date.now() - reAuthStartTime;
                  this.logger.error("Failed to re-authenticate or retry SObject create", {
                    sobjectType: type,
                    originalError: getErrorMessage(error),
                    retryError: getErrorMessage(retryError),
                    reAuthDuration,
                  });
                  throw retryError;
                }
              }
              throw error;
            }
          },
          { label: `salesforce:sobject:${type}:create` }
        );
      },

      update: async (data: object & { Id: string }) => {
        return this.requestQueue.executeHighPriority(
          async () => {
            try {
              await this.ensureConnected();
              return await originalSObject.update(data);
            } catch (error: unknown) {
              if (this.isSessionExpiredError(error)) {
                const reAuthStartTime = Date.now();
                this.logger.warn(
                  "Salesforce session expired during SObject update, attempting to re-authenticate",
                  {
                    sobjectType: type,
                    recordId: data.Id,
                    originalError: getErrorMessage(error),
                  }
                );

                try {
                  await this.connect(true);
                  const reAuthDuration = Date.now() - reAuthStartTime;
                  this.logger.debug("Retrying SObject update after re-authentication", {
                    sobjectType: type,
                    recordId: data.Id,
                    reAuthDuration,
                  });

                  const newSObject = this.connection.sobject(type);
                  return await newSObject.update(data);
                } catch (retryError) {
                  const reAuthDuration = Date.now() - reAuthStartTime;
                  this.logger.error("Failed to re-authenticate or retry SObject update", {
                    sobjectType: type,
                    recordId: data.Id,
                    originalError: getErrorMessage(error),
                    retryError: getErrorMessage(retryError),
                    reAuthDuration,
                  });
                  throw retryError;
                }
              }
              throw error;
            }
          },
          { label: `salesforce:sobject:${type}:update` }
        );
      },
    };
  }

  async compositeTree<T>(
    sobjectType: string,
    body: unknown,
    options: { allOrNone?: boolean; priority?: number; label?: string } = {}
  ): Promise<T> {
    const priority = options.priority ?? 5;
    const allOrNone = options.allOrNone ?? true;
    const path = this.buildCompositeTreePath(sobjectType, allOrNone);
    const label = options.label ?? `salesforce:composite:${sobjectType}`;

    return this.requestQueue.execute(
      async () => {
        await this.ensureConnected();

        if (!body || typeof body !== "object") {
          throw new TypeError("Salesforce composite tree body must be an object");
        }

        const payload = body as Record<string, unknown> | Record<string, unknown>[];

        try {
          return (await this.connection.requestPost(path, payload)) as T;
        } catch (error) {
          if (this.isSessionExpiredError(error)) {
            const reAuthStartTime = Date.now();
            this.logger.warn("Salesforce session expired during composite tree request, retrying", {
              sobjectType,
              originalError: getErrorMessage(error),
            });

            await this.connect(true);
            const reAuthDuration = Date.now() - reAuthStartTime;

            this.logger.debug("Retrying composite tree request after re-authentication", {
              sobjectType,
              reAuthDuration,
            });

            return (await this.connection.requestPost(path, payload)) as T;
          }

          throw error;
        }
      },
      { priority, label }
    );
  }

  private buildCompositeTreePath(sobjectType: string, allOrNone: boolean): string {
    const version = this.connection.version || "58.0";
    const normalizedVersion = version.startsWith("v") ? version.slice(1) : version;
    const encodedType = encodeURIComponent(sobjectType);
    const query = `?allOrNone=${allOrNone ? "true" : "false"}`;
    return `/services/data/v${normalizedVersion}/composite/tree/${encodedType}${query}`;
  }

  private deriveQueryLabel(soql: string): string {
    const match = /from\s+([a-z0-9_]+)/i.exec(soql);
    if (!match) {
      return "salesforce:query:unknown";
    }
    return `salesforce:query:${match[1]}`;
  }

  isConnected(): boolean {
    return !!this.connection.accessToken;
  }

  private getTokenTtl(): number {
    const configured = Number(this.configService.get("SF_TOKEN_TTL_MS"));
    if (Number.isFinite(configured) && configured > 0) {
      return configured;
    }
    // Default to 12 minutes (tokens typically expire after 15 minutes)
    return 12 * 60 * 1000;
  }

  private getRefreshBuffer(): number {
    const configured = Number(this.configService.get("SF_TOKEN_REFRESH_BUFFER_MS"));
    if (Number.isFinite(configured) && configured >= 0) {
      return configured;
    }
    return 60 * 1000; // 1 minute buffer
  }

  private isTokenExpiring(): boolean {
    if (!this.tokenExpiresAt) {
      return true;
    }
    return Date.now() + this.getRefreshBuffer() >= this.tokenExpiresAt;
  }

  async ensureConnected(): Promise<void> {
    const wasConnected = this.isConnected();
    const isExpiring = this.isTokenExpiring();

    if (!wasConnected || isExpiring) {
      this.logger.debug("Salesforce connection stale; refreshing access token", {
        wasConnected,
        isExpiring,
        tokenIssuedAt: this.tokenIssuedAt ? new Date(this.tokenIssuedAt).toISOString() : null,
        tokenExpiresAt: this.tokenExpiresAt ? new Date(this.tokenExpiresAt).toISOString() : null,
        currentTime: new Date().toISOString(),
      });

      await this.connect(!wasConnected);
    }
  }

  /**
   * Execute a high-priority Salesforce request (jumps queue)
   */
  async queryHighPriority(soql: string, options: { label?: string } = {}): Promise<unknown> {
    const label = options.label ?? `${this.deriveQueryLabel(soql)}:high`;
    return this.requestQueue.executeHighPriority(
      async () => {
        await this.ensureConnected();
        try {
          return await this.connection.query(soql);
        } catch (error: unknown) {
          if (this.isSessionExpiredError(error)) {
            const reAuthStartTime = Date.now();
            this.logger.warn(
              "Salesforce session expired during high-priority query, attempting to re-authenticate",
              {
                originalError: getErrorMessage(error),
              }
            );

            await this.connect(true);
            const reAuthDuration = Date.now() - reAuthStartTime;
            this.logger.debug("Retrying high-priority query after re-authentication", {
              reAuthDuration,
            });

            return await this.connection.query(soql);
          }
          throw error;
        }
      },
      { label }
    );
  }

  /**
   * Get queue metrics for monitoring
   */
  getQueueMetrics() {
    return this.requestQueue.getMetrics();
  }

  /**
   * Get queue health status
   */
  getQueueHealth() {
    return this.requestQueue.getHealthStatus();
  }

  /**
   * Get daily API usage information
   */
  getDailyUsage() {
    return this.requestQueue.getDailyUsage();
  }

  /**
   * Determine query priority based on SOQL content
   */
  private getQueryPriority(soql: string): number {
    const lowerSoql = soql.toLowerCase();

    // High priority queries (critical for user experience)
    if (
      lowerSoql.includes("account") ||
      lowerSoql.includes("user") ||
      lowerSoql.includes("where id =")
    ) {
      return 8;
    }

    // Medium priority queries
    if (
      lowerSoql.includes("order") ||
      lowerSoql.includes("invoice") ||
      lowerSoql.includes("limit 1")
    ) {
      return 5;
    }

    // Low priority (bulk queries, reports)
    return 2;
  }

  /**
   * Determine if query is long-running based on SOQL content
   */
  private isLongRunningQuery(soql: string): boolean {
    const lowerSoql = soql.toLowerCase();

    // Queries likely to take >20 seconds
    return (
      lowerSoql.includes("count(") ||
      lowerSoql.includes("group by") ||
      lowerSoql.includes("order by") ||
      (lowerSoql.includes("limit") && !lowerSoql.includes("limit 1")) ||
      lowerSoql.length > 500 // Very complex queries
    );
  }
}
