import { Injectable, Inject, BadRequestException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { ServicesCacheService } from "./services-cache.service.js";
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js";
import { WorkflowCaseManager } from "@bff/modules/shared/workflow/index.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import {
  assertSalesforceId,
  assertSoqlFieldName,
} from "@bff/integrations/salesforce/utils/soql.util.js";
import type {
  InternetEligibilityDetails,
  InternetEligibilityStatus,
} from "@customer-portal/domain/services";
import { internetEligibilityDetailsSchema } from "@customer-portal/domain/services";
import type { InternetEligibilityCheckRequest } from "./internet-eligibility.types.js";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";

@Injectable()
export class InternetEligibilityService {
  // eslint-disable-next-line max-params -- NestJS dependency injection requires all services to be injected via constructor
  constructor(
    private readonly sf: SalesforceConnection,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger,
    private readonly mappingsService: MappingsService,
    private readonly catalogCache: ServicesCacheService,
    private readonly opportunityResolution: OpportunityResolutionService,
    private readonly workflowCases: WorkflowCaseManager
  ) {}

  async getEligibilityForUser(userId: string): Promise<string | null> {
    const details = await this.getEligibilityDetailsForUser(userId);
    return details.eligibility;
  }

  async getEligibilityDetailsForUser(userId: string): Promise<InternetEligibilityDetails> {
    const mapping = await this.mappingsService.findByUserId(userId);
    if (!mapping) {
      return internetEligibilityDetailsSchema.parse({
        status: "not_requested",
        eligibility: null,
        requestId: null,
        requestedAt: null,
        checkedAt: null,
        notes: null,
      });
    }

    const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
    const eligibilityKey = this.catalogCache.buildEligibilityKey("internet", sfAccountId);

    // Explicitly define the validator to handle potential malformed cache data
    // If the cache returns undefined or missing fields, we treat it as a cache miss or malformed data
    // and force a re-fetch or ensure safe defaults are applied.
    return this.catalogCache
      .getCachedEligibility<InternetEligibilityDetails>(eligibilityKey, async () =>
        this.queryEligibilityDetails(sfAccountId)
      )
      .then(async data => {
        // Safety check: ensure the data matches the schema before returning.
        // This protects against cache corruption (e.g. missing fields treated as undefined).
        const result = internetEligibilityDetailsSchema.safeParse(data);
        if (!result.success) {
          this.logger.warn("Cached eligibility data was malformed, treating as cache miss", {
            userId,
            sfAccountId,
            errors: result.error.format(),
          });
          // Invalidate bad cache and re-fetch
          this.catalogCache.invalidateEligibility(sfAccountId).catch((error: unknown) =>
            this.logger.error("Failed to invalidate malformed eligibility cache", {
              error: extractErrorMessage(error),
            })
          );
          return this.queryEligibilityDetails(sfAccountId);
        }
        return result.data;
      });
  }

  async requestEligibilityCheckForUser(
    userId: string,
    request: InternetEligibilityCheckRequest
  ): Promise<string> {
    const mapping = await this.mappingsService.findByUserId(userId);
    if (!mapping?.sfAccountId) {
      throw new Error("No Salesforce mapping found for current user");
    }

    const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");

    if (
      !request.address ||
      !request.address.address1 ||
      !request.address.city ||
      !request.address.postcode
    ) {
      throw new BadRequestException("Service address is required to request eligibility review.");
    }

    try {
      // 1) Find or create Opportunity for Internet eligibility
      const { opportunityId, wasCreated: opportunityCreated } =
        await this.opportunityResolution.findOrCreateForInternetEligibility(sfAccountId);

      // 2) Create eligibility check case via WorkflowCaseManager
      // Build address object conditionally to avoid exactOptionalPropertyTypes issues
      const eligibilityAddress: Record<string, string> = {};
      if (request.address.address1) eligibilityAddress["address1"] = request.address.address1;
      if (request.address.address2) eligibilityAddress["address2"] = request.address.address2;
      if (request.address.city) eligibilityAddress["city"] = request.address.city;
      if (request.address.state) eligibilityAddress["state"] = request.address.state;
      if (request.address.postcode) eligibilityAddress["postcode"] = request.address.postcode;
      if (request.address.country) eligibilityAddress["country"] = request.address.country;

      // Note: streetAddress not passed in this flow (basic address type) - duplicate detection will be skipped
      await this.workflowCases.notifyEligibilityCheck({
        accountId: sfAccountId,
        opportunityId,
        opportunityCreated,
        address: eligibilityAddress as {
          address1?: string;
          address2?: string;
          city?: string;
          state?: string;
          postcode?: string;
          country?: string;
        },
      });

      // 3) Update Account eligibility status
      await this.updateAccountEligibilityRequestState(sfAccountId);
      await this.catalogCache.invalidateEligibility(sfAccountId);

      this.logger.log("Eligibility check request submitted", {
        userId,
        sfAccountIdTail: sfAccountId.slice(-4),
        opportunityIdTail: opportunityId.slice(-4),
        opportunityCreated,
      });

      // Return the opportunity ID as the request identifier
      return opportunityId;
    } catch (error) {
      this.logger.error("Failed to create eligibility request", {
        userId,
        sfAccountId,
        error: extractErrorMessage(error),
      });
      throw new Error("Failed to request availability check. Please try again later.");
    }
  }

  checkPlanEligibility(planOfferingType: string | null | undefined, eligibility: string): boolean {
    // Simple match: user's eligibility field must equal plan's Salesforce offering type
    // e.g., eligibility "Home 1G" matches plan.internetOfferingType "Home 1G"
    return planOfferingType === eligibility;
  }

  private async queryEligibilityDetails(sfAccountId: string): Promise<InternetEligibilityDetails> {
    const eligibilityField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_FIELD") ?? "Internet_Eligibility__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_FIELD"
    );
    const statusField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_STATUS_FIELD") ??
        "Internet_Eligibility_Status__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_STATUS_FIELD"
    );
    const requestedAtField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_REQUESTED_AT_FIELD") ??
        "Internet_Eligibility_Request_Date_Time__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_REQUESTED_AT_FIELD"
    );
    const checkedAtField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_CHECKED_AT_FIELD") ??
        "Internet_Eligibility_Checked_Date_Time__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_CHECKED_AT_FIELD"
    );

    const selectBase = `Id, ${eligibilityField}, ${statusField}, ${requestedAtField}, ${checkedAtField}`;

    const queryAccount = async (selectFields: string, labelSuffix: string) => {
      const soql = `
        SELECT ${selectFields}
        FROM Account
        WHERE Id = '${sfAccountId}'
        LIMIT 1
      `;
      return (await this.sf.query(soql, {
        label: `services:internet:eligibility_details:${labelSuffix}`,
      })) as SalesforceResponse<Record<string, unknown>>;
    };

    const res = await queryAccount(selectBase, "base");
    const record = res.records?.[0] ?? undefined;
    if (!record) {
      return internetEligibilityDetailsSchema.parse({
        status: "not_requested",
        eligibility: null,
        requestId: null,
        requestedAt: null,
        checkedAt: null,
        notes: null,
      });
    }

    const eligibilityRaw = record[eligibilityField];
    const eligibility =
      typeof eligibilityRaw === "string" && eligibilityRaw.trim().length > 0
        ? eligibilityRaw.trim()
        : null;

    const statusRaw = record[statusField];
    const normalizedStatus = typeof statusRaw === "string" ? statusRaw.trim().toLowerCase() : "";

    const statusMap: Record<string, InternetEligibilityStatus> = {
      pending: "pending",
      checking: "pending",
      eligible: "eligible",
      ineligible: "ineligible",
      "not available": "ineligible",
    };
    const status: InternetEligibilityStatus =
      statusMap[normalizedStatus] ?? (eligibility ? "eligible" : "not_requested");

    const requestedAtRaw = record[requestedAtField];
    const checkedAtRaw = record[checkedAtField];

    // Normalize datetime strings to ISO 8601 with Z suffix (Salesforce may return +0000 offset)
    const normalizeDateTime = (value: unknown): string | null => {
      if (value instanceof Date) {
        return value.toISOString();
      }
      if (typeof value === "string" && value.trim()) {
        const parsed = new Date(value);
        return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
      }
      return null;
    };

    const requestedAt = normalizeDateTime(requestedAtRaw);
    const checkedAt = normalizeDateTime(checkedAtRaw);

    return internetEligibilityDetailsSchema.parse({
      status,
      eligibility,
      requestId: null,
      requestedAt,
      checkedAt,
      notes: null,
    });
  }

  private async updateAccountEligibilityRequestState(sfAccountId: string): Promise<void> {
    const statusField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_STATUS_FIELD") ??
        "Internet_Eligibility_Status__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_STATUS_FIELD"
    );
    const requestedAtField = assertSoqlFieldName(
      this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_REQUESTED_AT_FIELD") ??
        "Internet_Eligibility_Request_Date_Time__c",
      "ACCOUNT_INTERNET_ELIGIBILITY_REQUESTED_AT_FIELD"
    );
    const update = this.sf.sobject("Account")?.update;
    if (!update) {
      throw new Error("Salesforce Account update method not available");
    }

    const basePayload: { Id: string } & Record<string, unknown> = {
      Id: sfAccountId,
      [statusField]: "Pending",
      [requestedAtField]: new Date().toISOString(),
    };
    await update(basePayload);
  }
}
