/**
 * Salesforce Case Integration Service
 *
 * Unified service for all Salesforce Case operations.
 *
 * Case Types:
 * - Customer Support Cases (Origin: Portal Support) - visible to customers
 * - Internal Workflow Cases (Origin: Portal Notification) - for CS team only
 * - Web-to-Case (Origin: Web) - public contact form submissions
 *
 * Uses domain types and mappers from @customer-portal/domain/support
 */

import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId } from "../utils/soql.util.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { SalesforceOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { CASE_FIELDS } from "../constants/field-maps.js";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import type { SupportCase, CaseMessageList } from "@customer-portal/domain/support";
import {
  type SalesforceCaseRecord,
  type SalesforceEmailMessage,
  type SalesforceCaseComment,
  type SalesforceCaseOrigin,
  SALESFORCE_CASE_ORIGIN,
  SALESFORCE_CASE_STATUS,
  SALESFORCE_CASE_PRIORITY,
  toSalesforcePriority,
  buildCaseByIdQuery,
  buildCaseSelectFields,
  buildCasesForAccountQuery,
  buildEmailMessagesForCaseQuery,
  buildCaseCommentsForCaseQuery,
  transformSalesforceCaseToSupportCase,
  transformSalesforceCasesToSupportCases,
  transformEmailMessagesToCaseMessages,
  transformCaseCommentsToCaseMessages,
  mergeAndSortCaseMessages,
} from "@customer-portal/domain/support/providers";

// ============================================================================
// Types
// ============================================================================

/**
 * Salesforce Case record payload.
 * Keys are dynamic (resolved from CASE_FIELDS constant at runtime),
 * so a static interface is not possible.
 */
type SalesforceCasePayload = Record<string, unknown>;

/**
 * Parameters for creating any case in Salesforce.
 *
 * The `origin` field determines case visibility:
 * - PORTAL_SUPPORT: Customer-visible support case
 * - PORTAL_NOTIFICATION: Internal CS workflow case (eligibility, verification, cancellation)
 */
export interface CreateCaseParams {
  /** Salesforce Account ID */
  accountId: string;
  /** Case subject line */
  subject: string;
  /** Case description/body */
  description: string;
  /** Case origin - determines visibility and routing */
  origin: SalesforceCaseOrigin;
  /** Priority (defaults to Medium) */
  priority?: string | undefined;
  /** Optional Salesforce Contact ID */
  contactId?: string | undefined;
  /** Optional Opportunity ID for workflow cases */
  opportunityId?: string | undefined;
  /** Eligibility address key for duplicate detection (postcode:streetAddress) */
  eligibilityAddressKey?: string | undefined;
}

/**
 * Parameters for Web-to-Case (public contact form, no account)
 */
export interface CreateWebCaseParams {
  subject: string;
  description: string;
  suppliedEmail: string;
  suppliedName: string;
  suppliedPhone?: string | undefined;
  origin?: string | undefined;
  priority?: string | undefined;
}

@Injectable()
export class SalesforceCaseService {
  constructor(
    private readonly sf: SalesforceConnection,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Get all customer-visible support cases for an account
   */
  async getCasesForAccount(accountId: string): Promise<SupportCase[]> {
    const safeAccountId = assertSalesforceId(accountId, "accountId");
    this.logger.debug({ accountId: safeAccountId }, "Fetching portal cases for account");

    const soql = buildCasesForAccountQuery(safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_SUPPORT);

    try {
      const result = (await this.sf.query(soql, {
        label: "support:listCasesForAccount",
      })) as SalesforceResponse<SalesforceCaseRecord>;

      const cases = result.records || [];

      this.logger.debug(
        { accountId: safeAccountId, caseCount: cases.length },
        "Portal cases retrieved for account"
      );

      return transformSalesforceCasesToSupportCases(cases);
    } catch (error: unknown) {
      this.logger.error("Failed to fetch cases for account", {
        error: extractErrorMessage(error),
        accountId: safeAccountId,
      });
      throw new SalesforceOperationException("Failed to fetch support cases");
    }
  }

  /**
   * Get a single case by ID with account ownership validation
   */
  async getCaseById(caseId: string, accountId: string): Promise<SupportCase | null> {
    const safeCaseId = assertSalesforceId(caseId, "caseId");
    const safeAccountId = assertSalesforceId(accountId, "accountId");

    this.logger.debug({ caseId: safeCaseId, accountId: safeAccountId }, "Fetching case by ID");

    const soql = buildCaseByIdQuery(
      safeCaseId,
      safeAccountId,
      SALESFORCE_CASE_ORIGIN.PORTAL_SUPPORT
    );

    try {
      const result = (await this.sf.query(soql, {
        label: "support:getCaseById",
      })) as SalesforceResponse<SalesforceCaseRecord>;

      const record = result.records?.[0];

      if (!record) {
        this.logger.debug({ caseId: safeCaseId }, "Case not found or access denied");
        return null;
      }

      return transformSalesforceCaseToSupportCase(record);
    } catch (error: unknown) {
      this.logger.error("Failed to fetch case by ID", {
        error: extractErrorMessage(error),
        caseId: safeCaseId,
      });
      throw new SalesforceOperationException("Failed to fetch support case");
    }
  }

  /**
   * Create a case in Salesforce.
   *
   * This is the unified method for creating all case types:
   * - Customer support cases (origin: PORTAL_SUPPORT)
   * - Internal workflow cases (origin: PORTAL_NOTIFICATION)
   *
   * @param params - Case creation parameters
   * @returns Created case ID and case number
   */
  async createCase(params: CreateCaseParams): Promise<{ id: string; caseNumber: string }> {
    const safeAccountId = assertSalesforceId(params.accountId, "accountId");
    const safeContactId = params.contactId
      ? assertSalesforceId(params.contactId, "contactId")
      : undefined;
    const safeOpportunityId = params.opportunityId
      ? assertSalesforceId(params.opportunityId, "opportunityId")
      : undefined;

    const isInternalCase = params.origin === SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION;

    this.logger.log("Creating Salesforce case", {
      accountIdTail: safeAccountId.slice(-4),
      origin: params.origin,
      isInternal: isInternalCase,
      hasOpportunity: !!safeOpportunityId,
    });

    // Convert portal display priority to Salesforce API value
    const sfPriority = params.priority
      ? toSalesforcePriority(params.priority)
      : SALESFORCE_CASE_PRIORITY.MEDIUM;

    const casePayload: SalesforceCasePayload = {
      [CASE_FIELDS.origin]: params.origin,
      [CASE_FIELDS.status]: SALESFORCE_CASE_STATUS.NEW,
      [CASE_FIELDS.priority]: sfPriority,
      [CASE_FIELDS.subject]: params.subject.trim(),
      [CASE_FIELDS.description]: params.description.trim(),
    };

    // Set ContactId if available - Salesforce will auto-populate AccountId from Contact
    // If no ContactId, we must set AccountId directly
    if (safeContactId) {
      casePayload[CASE_FIELDS.contactId] = safeContactId;
    } else {
      casePayload[CASE_FIELDS.accountId] = safeAccountId;
    }

    // Link to Opportunity if provided (used for workflow cases)
    if (safeOpportunityId) {
      casePayload[CASE_FIELDS.opportunityId] = safeOpportunityId;
    }

    // Add eligibility address key for duplicate detection (if provided)
    if (params.eligibilityAddressKey) {
      casePayload[CASE_FIELDS.eligibilityAddressKey] = params.eligibilityAddressKey;
    }

    try {
      const created = (await this.sf.sobject("Case").create(casePayload)) as { id?: string };

      if (!created.id) {
        throw new SalesforceOperationException("Salesforce did not return a case ID");
      }

      // Fetch the created case to get the CaseNumber
      const createdCase = await this.getCaseByIdInternal(created.id);
      const caseNumber = createdCase?.CaseNumber ?? created.id;

      this.logger.log("Salesforce case created successfully", {
        caseId: created.id,
        caseNumber,
        origin: params.origin,
      });

      return { id: created.id, caseNumber };
    } catch (error: unknown) {
      this.logger.error("Failed to create Salesforce case", {
        error: extractErrorMessage(error),
        accountIdTail: safeAccountId.slice(-4),
        origin: params.origin,
      });
      throw new SalesforceOperationException("Failed to create case");
    }
  }

  /**
   * Create a Web-to-Case for public contact form submissions.
   *
   * Does not require an Account - uses supplied contact info.
   * Separate from createCase() because it has a different payload structure.
   */
  async createWebCase(params: CreateWebCaseParams): Promise<{ id: string; caseNumber: string }> {
    this.logger.log("Creating Web-to-Case", { email: params.suppliedEmail });

    const casePayload: SalesforceCasePayload = {
      [CASE_FIELDS.origin]: params.origin ?? "Web",
      [CASE_FIELDS.status]: SALESFORCE_CASE_STATUS.NEW,
      [CASE_FIELDS.priority]: params.priority ?? SALESFORCE_CASE_PRIORITY.MEDIUM,
      [CASE_FIELDS.subject]: params.subject.trim(),
      [CASE_FIELDS.description]: params.description.trim(),
      [CASE_FIELDS.suppliedEmail]: params.suppliedEmail,
      [CASE_FIELDS.suppliedName]: params.suppliedName,
      [CASE_FIELDS.suppliedPhone]: params.suppliedPhone ?? null,
    };

    try {
      const created = (await this.sf.sobject("Case").create(casePayload)) as { id?: string };

      if (!created.id) {
        throw new SalesforceOperationException("Salesforce did not return a case ID");
      }

      // Fetch the created case to get the CaseNumber
      const createdCase = await this.getCaseByIdInternal(created.id);
      const caseNumber = createdCase?.CaseNumber ?? created.id;

      this.logger.log("Web-to-Case created successfully", {
        caseId: created.id,
        caseNumber,
        email: params.suppliedEmail,
      });

      return { id: created.id, caseNumber };
    } catch (error: unknown) {
      this.logger.error("Failed to create Web-to-Case", {
        error: extractErrorMessage(error),
        email: params.suppliedEmail,
      });
      throw new SalesforceOperationException("Failed to create contact request");
    }
  }

  /**
   * Internal method to fetch case without account validation (for post-create lookup)
   */
  private async getCaseByIdInternal(caseId: string): Promise<SalesforceCaseRecord | null> {
    const safeCaseId = assertSalesforceId(caseId, "caseId");

    const fields = buildCaseSelectFields().join(", ");
    const soql = `
      SELECT ${fields}
      FROM Case
      WHERE Id = '${safeCaseId}'
      LIMIT 1
    `;

    const result = (await this.sf.query(soql, {
      label: "support:getCaseByIdInternal",
    })) as SalesforceResponse<SalesforceCaseRecord>;

    return result.records?.[0] ?? null;
  }

  // ============================================================================
  // Case Messages (Conversation) Methods
  // ============================================================================

  /**
   * Get all messages (EmailMessages + public CaseComments) for a case.
   *
   * Returns a unified, chronologically sorted conversation thread.
   *
   * @param caseId - Salesforce Case ID
   * @param accountId - Account ID for ownership validation
   * @param customerEmail - Customer's email for identifying their messages
   * @returns Unified message list with thread ID
   */
  async getCaseMessages(
    caseId: string,
    accountId: string,
    customerEmail?: string
  ): Promise<CaseMessageList> {
    const safeCaseId = assertSalesforceId(caseId, "caseId");
    const safeAccountId = assertSalesforceId(accountId, "accountId");

    this.logger.debug({ caseId: safeCaseId }, "Fetching case messages");

    // First verify case belongs to this account
    const caseRecord = await this.getCaseById(safeCaseId, safeAccountId);
    if (!caseRecord) {
      this.logger.debug({ caseId: safeCaseId }, "Case not found or access denied for messages");
      return { messages: [], threadId: null };
    }

    try {
      // Fetch EmailMessages and CaseComments in parallel
      const [emailsResult, commentsResult] = await Promise.all([
        this.sf.query(buildEmailMessagesForCaseQuery(safeCaseId), {
          label: "support:getCaseEmailMessages",
        }) as Promise<SalesforceResponse<SalesforceEmailMessage>>,
        this.sf.query(buildCaseCommentsForCaseQuery(safeCaseId), {
          label: "support:getCaseComments",
        }) as Promise<SalesforceResponse<SalesforceCaseComment>>,
      ]);

      const emails = emailsResult.records || [];
      const comments = commentsResult.records || [];

      // Transform to unified CaseMessage format
      const emailMessages = transformEmailMessagesToCaseMessages(emails, customerEmail);
      const commentMessages = transformCaseCommentsToCaseMessages(comments, customerEmail);

      // Merge and sort chronologically
      const messages = mergeAndSortCaseMessages(emailMessages, commentMessages);

      // Extract thread ID from first email if available
      const threadId = emails[0]?.ThreadIdentifier ?? null;

      this.logger.debug(
        {
          caseId: safeCaseId,
          emailCount: emails.length,
          commentCount: comments.length,
          totalMessages: messages.length,
        },
        "Case messages retrieved"
      );

      return { messages, threadId };
    } catch (error: unknown) {
      this.logger.error("Failed to fetch case messages", {
        error: extractErrorMessage(error),
        caseId: safeCaseId,
      });
      throw new SalesforceOperationException("Failed to fetch case messages");
    }
  }

  /**
   * Add a comment to a case (customer reply via portal).
   *
   * Creates a public CaseComment that will be visible to both
   * the customer and support agents.
   *
   * @param caseId - Salesforce Case ID
   * @param accountId - Account ID for ownership validation
   * @param body - Comment text
   * @returns Created comment ID and timestamp
   */
  async addCaseComment(
    caseId: string,
    accountId: string,
    body: string
  ): Promise<{ id: string; createdAt: string }> {
    const safeCaseId = assertSalesforceId(caseId, "caseId");
    const safeAccountId = assertSalesforceId(accountId, "accountId");

    // First verify case belongs to this account
    const caseRecord = await this.getCaseById(safeCaseId, safeAccountId);
    if (!caseRecord) {
      this.logger.warn(
        { caseId: safeCaseId },
        "Attempted to add comment to non-existent/unauthorized case"
      );
      throw new SalesforceOperationException("Case not found");
    }

    this.logger.log("Adding comment to case", {
      caseId: safeCaseId,
      bodyLength: body.length,
    });

    try {
      const commentPayload = {
        ParentId: safeCaseId,
        CommentBody: body.trim(),
        IsPublished: true, // Visible to customer
      };

      const created = (await this.sf.sobject("CaseComment").create(commentPayload)) as {
        id?: string;
      };

      if (!created.id) {
        throw new SalesforceOperationException("Salesforce did not return a comment ID");
      }

      const createdAt = new Date().toISOString();

      this.logger.log("Case comment created successfully", {
        caseId: safeCaseId,
        commentId: created.id,
      });

      return { id: created.id, createdAt };
    } catch (error: unknown) {
      this.logger.error("Failed to create case comment", {
        error: extractErrorMessage(error),
        caseId: safeCaseId,
      });
      throw new SalesforceOperationException("Failed to add comment");
    }
  }
}
