import { Injectable, Inject, HttpStatus } 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 { WorkflowCaseManager } from "@bff/modules/shared/workflow/index.js";
import { ServicesCacheService } from "@bff/modules/services/application/services-cache.service.js";
import {
  assertSalesforceId,
  assertSoqlFieldName,
} from "@bff/integrations/salesforce/utils/soql.util.js";
import { normalizeSalesforceDateTimeToIsoUtc } from "@bff/integrations/salesforce/utils/datetime.util.js";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { ErrorCode } from "@customer-portal/domain/common";
import {
  residenceCardVerificationSchema,
  type ResidenceCardVerification,
  type ResidenceCardVerificationStatus,
} from "@customer-portal/domain/customer";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { DomainHttpException } from "@bff/core/http/domain-http.exception.js";
import { basename, extname } from "node:path";

// Default fallback filename for residence card submissions
const DEFAULT_FILENAME = "residence-card";

function extractNonEmptyString(value: unknown): string | null {
  if (typeof value === "string") {
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : null;
  }
  return null;
}

@Injectable()
export class ResidenceCardService {
  // eslint-disable-next-line max-params -- NestJS DI requires injecting multiple services
  constructor(
    private readonly sf: SalesforceConnection,
    private readonly mappings: MappingsService,
    private readonly workflowCases: WorkflowCaseManager,
    private readonly servicesCache: ServicesCacheService,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async getStatusForUser(userId: string): Promise<ResidenceCardVerification> {
    const mapping = await this.mappings.findByUserId(userId);
    if (!mapping) {
      return residenceCardVerificationSchema.parse({
        status: "not_submitted",
        submittedAt: null,
        reviewedAt: null,
        reviewerNotes: null,
      });
    }
    const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");

    return this.servicesCache.getCachedVerification(sfAccountId, async () => {
      return this.fetchVerificationFromSalesforce(sfAccountId);
    });
  }

  private async fetchVerificationFromSalesforce(
    sfAccountId: string
  ): Promise<ResidenceCardVerification> {
    const fields = this.getAccountFieldNames();
    const soql = `
      SELECT Id, ${fields.status}, ${fields.submittedAt}, ${fields.verifiedAt}, ${fields.note}, ${fields.rejectionMessage}
      FROM Account
      WHERE Id = '${sfAccountId}'
      LIMIT 1
    `;

    const accountRes = (await this.sf.query(soql, {
      label: "verification:residence_card:account",
    })) as SalesforceResponse<Record<string, unknown>>;

    const account = accountRes.records?.[0] ?? undefined;
    const statusRaw = account ? account[fields.status] : undefined;
    const statusText = typeof statusRaw === "string" ? statusRaw.trim().toLowerCase() : "";

    const statusMap: Record<string, ResidenceCardVerificationStatus> = {
      verified: "verified",
      rejected: "rejected",
      submitted: "pending",
      "not submitted": "not_submitted",
      not_submitted: "not_submitted",
      "": "not_submitted",
    };
    const status: ResidenceCardVerificationStatus = statusMap[statusText] ?? "pending";

    const submittedAtRaw = account ? account[fields.submittedAt] : undefined;
    const verifiedAtRaw = account ? account[fields.verifiedAt] : undefined;
    const noteRaw = account ? account[fields.note] : undefined;
    const rejectionRaw = account ? account[fields.rejectionMessage] : undefined;

    const submittedAt = normalizeSalesforceDateTimeToIsoUtc(submittedAtRaw);
    const reviewedAt = normalizeSalesforceDateTimeToIsoUtc(verifiedAtRaw);

    const reviewerNotes =
      extractNonEmptyString(rejectionRaw) ?? extractNonEmptyString(noteRaw) ?? null;

    return residenceCardVerificationSchema.parse({
      status,
      submittedAt,
      reviewedAt,
      reviewerNotes,
    });
  }

  async submitForUser(params: {
    userId: string;
    filename: string;
    mimeType: string;
    sizeBytes: number;
    content: Buffer;
  }): Promise<ResidenceCardVerification> {
    const mapping = await this.mappings.findByUserId(params.userId);
    if (!mapping?.sfAccountId) {
      throw new DomainHttpException(ErrorCode.ACCOUNT_MAPPING_MISSING, HttpStatus.BAD_REQUEST);
    }
    const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
    const filename = params.filename || DEFAULT_FILENAME;

    // Create an internal workflow Case for CS to track this submission.
    // (No lock/dedupe: multiple submissions may create multiple cases by design.)
    const caseId = await this.workflowCases.createIdVerificationCase({
      accountId: sfAccountId,
      filename,
      mimeType: params.mimeType,
      sizeBytes: params.sizeBytes,
    });

    await this.uploadFileToCase(params, caseId, filename);
    await this.updateAccountVerificationStatus(sfAccountId);

    // Invalidate cache so next fetch gets fresh data
    await this.servicesCache.invalidateVerification(sfAccountId);

    return this.getStatusForUser(params.userId);
  }

  private async uploadFileToCase(
    params: { userId: string; filename: string; content: Buffer },
    caseId: string,
    filename: string
  ): Promise<void> {
    const versionData = params.content.toString("base64");
    const extension = extname(filename).replace(/^\./, "");
    const title = basename(filename, extension ? `.${extension}` : "");

    const create = this.sf.sobject("ContentVersion")?.create;
    if (!create) {
      throw new DomainHttpException(ErrorCode.CONFIGURATION_ERROR, HttpStatus.SERVICE_UNAVAILABLE);
    }

    try {
      // Attach file directly to the Case (not Account) so CS can see it in the Case's Files
      const result = await create({
        Title: title || DEFAULT_FILENAME,
        PathOnClient: filename,
        VersionData: versionData,
        FirstPublishLocationId: caseId, // Attach to Case, not Account
      });
      const contentVersionId = (result as { id?: unknown })?.id;
      if (typeof contentVersionId !== "string" || contentVersionId.trim().length === 0) {
        throw new DomainHttpException(
          ErrorCode.EXTERNAL_SERVICE_ERROR,
          HttpStatus.SERVICE_UNAVAILABLE
        );
      }

      this.logger.log("ID verification file attached to Case", {
        caseIdTail: caseId.slice(-4),
        contentVersionId,
        filename: params.filename,
      });
    } catch (error) {
      if (error instanceof DomainHttpException) {
        throw error;
      }
      this.logger.error("Failed to upload residence card to Salesforce Files", {
        userId: params.userId,
        caseIdTail: caseId.slice(-4),
        error: extractErrorMessage(error),
      });
      throw new DomainHttpException(
        ErrorCode.EXTERNAL_SERVICE_ERROR,
        HttpStatus.SERVICE_UNAVAILABLE
      );
    }
  }

  private async updateAccountVerificationStatus(sfAccountId: string): Promise<void> {
    const fields = this.getAccountFieldNames();
    const update = this.sf.sobject("Account")?.update;
    if (!update) {
      throw new DomainHttpException(ErrorCode.CONFIGURATION_ERROR, HttpStatus.SERVICE_UNAVAILABLE);
    }

    await update({
      Id: sfAccountId,
      [fields.status]: "Submitted",
      [fields.submittedAt]: new Date().toISOString(),
      [fields.rejectionMessage]: null,
      [fields.note]: null,
    });
  }

  private getAccountFieldNames(): {
    status: string;
    submittedAt: string;
    verifiedAt: string;
    note: string;
    rejectionMessage: string;
  } {
    return {
      status: assertSoqlFieldName(
        this.config.get<string>("ACCOUNT_ID_VERIFICATION_STATUS_FIELD") ??
          "Id_Verification_Status__c",
        "ACCOUNT_ID_VERIFICATION_STATUS_FIELD"
      ),
      submittedAt: assertSoqlFieldName(
        this.config.get<string>("ACCOUNT_ID_VERIFICATION_SUBMITTED_AT_FIELD") ??
          "Id_Verification_Submitted_Date_Time__c",
        "ACCOUNT_ID_VERIFICATION_SUBMITTED_AT_FIELD"
      ),
      verifiedAt: assertSoqlFieldName(
        this.config.get<string>("ACCOUNT_ID_VERIFICATION_VERIFIED_AT_FIELD") ??
          "Id_Verification_Verified_Date_Time__c",
        "ACCOUNT_ID_VERIFICATION_VERIFIED_AT_FIELD"
      ),
      note: assertSoqlFieldName(
        this.config.get<string>("ACCOUNT_ID_VERIFICATION_NOTE_FIELD") ?? "Id_Verification_Note__c",
        "ACCOUNT_ID_VERIFICATION_NOTE_FIELD"
      ),
      rejectionMessage: assertSoqlFieldName(
        this.config.get<string>("ACCOUNT_ID_VERIFICATION_REJECTION_MESSAGE_FIELD") ??
          "Id_Verification_Rejection_Message__c",
        "ACCOUNT_ID_VERIFICATION_REJECTION_MESSAGE_FIELD"
      ),
    };
  }
}
