import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
  Inject,
} from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { ZodError } from "zod";
import { IdMappingRepository } from "@bff/infra/database/repositories/index.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { MappingCacheService } from "./cache/mapping-cache.service.js";
import type {
  UserIdMapping,
  CreateMappingRequest,
  UpdateMappingRequest,
  MappingSearchFilters,
  MappingStats,
} from "./domain/index.js";
import {
  createMappingRequestSchema,
  updateMappingRequestSchema,
  validateNoConflicts,
  validateDeletion,
  sanitizeCreateRequest,
  sanitizeUpdateRequest,
} from "./domain/index.js";
import type { Prisma, IdMapping as PrismaIdMapping } from "@prisma/client";
import { mapPrismaMappingToDomain } from "@bff/infra/mappers/index.js";

@Injectable()
export class MappingsService {
  constructor(
    private readonly idMappingRepository: IdMappingRepository,
    private readonly cacheService: MappingCacheService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async createMapping(request: CreateMappingRequest): Promise<UserIdMapping> {
    try {
      // Validate and sanitize the request using schema
      let validatedRequest: CreateMappingRequest;
      try {
        const sanitized = sanitizeCreateRequest(request);
        validatedRequest = createMappingRequestSchema.parse(sanitized);
      } catch (error) {
        if (error instanceof ZodError) {
          const errors = error.issues.map(issue => issue.message);
          this.logger.warn({ request, errors }, "Create mapping request validation failed");
          throw new BadRequestException(`Invalid mapping data: ${errors.join(", ")}`);
        }
        throw error;
      }

      const sanitizedRequest = validatedRequest;

      const [byUser, byWhmcs, bySf] = await Promise.all([
        this.idMappingRepository.findById({ userId: sanitizedRequest.userId }),
        this.idMappingRepository.findById({
          whmcsClientId: sanitizedRequest.whmcsClientId,
        }),
        sanitizedRequest.sfAccountId
          ? this.idMappingRepository.findOne({ sfAccountId: sanitizedRequest.sfAccountId })
          : Promise.resolve(null),
      ]);

      const existingMappings = [byUser, byWhmcs, bySf]
        .filter((mapping): mapping is PrismaIdMapping => mapping !== null)
        .map(mapPrismaMappingToDomain);

      const conflictCheck = validateNoConflicts(sanitizedRequest, existingMappings);
      const warnings = conflictCheck.warnings;

      if (!conflictCheck.isValid) {
        throw new ConflictException(conflictCheck.errors.join("; "));
      }

      let created;
      try {
        const prismaData = {
          userId: sanitizedRequest.userId,
          whmcsClientId: sanitizedRequest.whmcsClientId,
          sfAccountId: sanitizedRequest.sfAccountId,
        };
        created = await this.idMappingRepository.create(prismaData);
      } catch (e) {
        const msg = extractErrorMessage(e);
        if (msg.includes("P2002") || /unique/i.test(msg)) {
          throw new ConflictException("Mapping violates uniqueness constraints");
        }
        throw e;
      }

      const mapping = mapPrismaMappingToDomain(created);

      await this.cacheService.setMapping(mapping);

      this.logger.log(`Created mapping for user ${mapping.userId}`, {
        whmcsClientId: mapping.whmcsClientId,
        sfAccountId: mapping.sfAccountId,
        warnings,
      });

      return mapping;
    } catch (error) {
      this.logger.error(`Failed to create mapping for user ${request.userId}`, {
        error: extractErrorMessage(error),
        request: this.sanitizeForLog(request),
      });
      throw error;
    }
  }

  async findBySfAccountId(sfAccountId: string): Promise<UserIdMapping | null> {
    try {
      if (!sfAccountId) {
        throw new BadRequestException("Salesforce Account ID is required");
      }

      const cached = await this.cacheService.getBySfAccountId(sfAccountId);
      if (cached) {
        this.logger.debug(`Cache hit for SF account mapping: ${sfAccountId}`);
        return cached;
      }

      const dbMapping = await this.idMappingRepository.findOne({ sfAccountId });
      if (!dbMapping) {
        this.logger.debug(`No mapping found for SF account ${sfAccountId}`);
        return null;
      }

      const mapping = mapPrismaMappingToDomain(dbMapping);

      await this.cacheService.setMapping(mapping);
      this.logger.debug(`Found mapping for SF account ${sfAccountId}`, {
        userId: mapping.userId,
        whmcsClientId: mapping.whmcsClientId,
      });
      return mapping;
    } catch (error) {
      this.logger.error(`Failed to find mapping for SF account ${sfAccountId}`, {
        error: extractErrorMessage(error),
      });
      throw error;
    }
  }

  async findByUserId(userId: string): Promise<UserIdMapping | null> {
    try {
      if (!userId) {
        throw new BadRequestException("User ID is required");
      }

      const cached = await this.cacheService.getByUserId(userId);
      if (cached) {
        this.logger.debug(`Cache hit for user mapping: ${userId}`);
        return cached;
      }

      const dbMapping = await this.idMappingRepository.findById({ userId });
      if (!dbMapping) {
        this.logger.debug(`No mapping found for user ${userId}`);
        return null;
      }

      const mapping = mapPrismaMappingToDomain(dbMapping);

      await this.cacheService.setMapping(mapping);
      this.logger.debug(`Found mapping for user ${userId}`, {
        whmcsClientId: mapping.whmcsClientId,
        sfAccountId: mapping.sfAccountId,
      });
      return mapping;
    } catch (error) {
      this.logger.error(`Failed to find mapping for user ${userId}`, {
        error: extractErrorMessage(error),
      });
      throw error;
    }
  }

  async findByWhmcsClientId(whmcsClientId: number): Promise<UserIdMapping | null> {
    try {
      if (!whmcsClientId || whmcsClientId < 1) {
        throw new BadRequestException("Valid WHMCS client ID is required");
      }

      const cached = await this.cacheService.getByWhmcsClientId(whmcsClientId);
      if (cached) {
        this.logger.debug(`Cache hit for WHMCS client mapping: ${whmcsClientId}`);
        return cached;
      }

      const dbMapping = await this.idMappingRepository.findById({ whmcsClientId });
      if (!dbMapping) {
        this.logger.debug(`No mapping found for WHMCS client ${whmcsClientId}`);
        return null;
      }

      const mapping = mapPrismaMappingToDomain(dbMapping);

      await this.cacheService.setMapping(mapping);
      this.logger.debug(`Found mapping for WHMCS client ${whmcsClientId}`, {
        userId: mapping.userId,
        sfAccountId: mapping.sfAccountId,
      });
      return mapping;
    } catch (error) {
      this.logger.error(`Failed to find mapping for WHMCS client ${whmcsClientId}`, {
        error: extractErrorMessage(error),
      });
      throw error;
    }
  }

  async updateMapping(userId: string, updates: UpdateMappingRequest): Promise<UserIdMapping> {
    try {
      // Validate and sanitize the updates using schema
      let validatedUpdates: UpdateMappingRequest;
      try {
        const sanitized = sanitizeUpdateRequest(updates);
        validatedUpdates = updateMappingRequestSchema.parse(sanitized);
      } catch (error) {
        if (error instanceof ZodError) {
          const errors = error.issues.map(issue => issue.message);
          this.logger.warn({ userId, updates, errors }, "Update mapping request validation failed");
          throw new BadRequestException(`Invalid update data: ${errors.join(", ")}`);
        }
        throw error;
      }

      const existing = await this.findByUserId(userId);
      if (!existing) {
        throw new NotFoundException(`Mapping not found for user ${userId}`);
      }

      const sanitizedUpdates = validatedUpdates;
      if (
        sanitizedUpdates.whmcsClientId &&
        sanitizedUpdates.whmcsClientId !== existing.whmcsClientId
      ) {
        const conflictingMapping = await this.findByWhmcsClientId(sanitizedUpdates.whmcsClientId);
        if (conflictingMapping && conflictingMapping.userId !== userId) {
          throw new ConflictException(
            `WHMCS client ${sanitizedUpdates.whmcsClientId} is already mapped to user ${conflictingMapping.userId}`
          );
        }
      }

      const prismaUpdateData: Prisma.IdMappingUpdateInput = {
        ...(sanitizedUpdates.whmcsClientId !== undefined && {
          whmcsClientId: sanitizedUpdates.whmcsClientId,
        }),
        ...(sanitizedUpdates.sfAccountId !== undefined && {
          sfAccountId: sanitizedUpdates.sfAccountId,
        }),
      };
      const updated = await this.idMappingRepository.update({ userId }, prismaUpdateData);

      const newMapping = mapPrismaMappingToDomain(updated);

      await this.cacheService.updateMapping(existing, newMapping);
      this.logger.log(`Updated mapping for user ${userId}`, {
        changes: sanitizedUpdates,
      });
      return newMapping;
    } catch (error) {
      this.logger.error(`Failed to update mapping for user ${userId}`, {
        error: extractErrorMessage(error),
        updates: this.sanitizeForLog(updates),
      });
      throw error;
    }
  }

  async deleteMapping(userId: string): Promise<void> {
    try {
      const existing = await this.findByUserId(userId);
      if (!existing) {
        throw new NotFoundException(`Mapping not found for user ${userId}`);
      }

      // Check deletion warnings (business logic)
      const validation = validateDeletion(existing);
      if (validation.warnings.length > 0) {
        this.logger.debug("Deletion warnings", { warnings: validation.warnings });
      }

      await this.idMappingRepository.delete({ userId });
      await this.cacheService.deleteMapping(existing);
      this.logger.log(`Deleted mapping for user ${userId}`, {
        whmcsClientId: existing.whmcsClientId,
        sfAccountId: existing.sfAccountId,
        warnings: validation.warnings,
      });
    } catch (error) {
      this.logger.error(`Failed to delete mapping for user ${userId}`, {
        error: extractErrorMessage(error),
      });
      throw error;
    }
  }

  async searchMappings(filters: MappingSearchFilters): Promise<UserIdMapping[]> {
    try {
      const whereClause: Prisma.IdMappingWhereInput = {};
      if (filters.userId) whereClause.userId = filters.userId;
      if (filters.whmcsClientId) whereClause.whmcsClientId = filters.whmcsClientId;
      if (filters.sfAccountId) whereClause.sfAccountId = filters.sfAccountId;
      if (filters.hasWhmcsMapping !== undefined) {
        if (filters.hasWhmcsMapping) {
          whereClause.whmcsClientId = { gt: 0 };
        } else {
          this.logger.debug(
            "Filtering mappings without WHMCS client IDs (expected to be empty until optional linking ships)"
          );
          whereClause.NOT = { whmcsClientId: { gt: 0 } };
        }
      }
      // Note: hasSfMapping filter is deprecated - sfAccountId is now required on all mappings
      // hasSfMapping: true matches all records, hasSfMapping: false matches none

      const dbMappings = await this.idMappingRepository.findMany(whereClause, {
        orderBy: { createdAt: "desc" },
      });
      const mappings = dbMappings.map(mapping => mapPrismaMappingToDomain(mapping));
      this.logger.debug(`Found ${mappings.length} mappings matching filters`, filters);
      return mappings;
    } catch (error) {
      this.logger.error("Failed to search mappings", {
        error: extractErrorMessage(error),
        filters,
      });
      throw error;
    }
  }

  async getMappingStats(): Promise<MappingStats> {
    try {
      // Since sfAccountId is now required, all mappings have SF accounts
      // and completeMappings equals whmcsMappings (orphanedMappings is always 0)
      const [totalCount, whmcsCount] = await Promise.all([
        this.idMappingRepository.count(),
        this.idMappingRepository.count({ whmcsClientId: { gt: 0 } }),
      ]);

      const stats: MappingStats = {
        totalMappings: totalCount,
        whmcsMappings: whmcsCount,
        salesforceMappings: totalCount, // All mappings now have sfAccountId
        completeMappings: whmcsCount, // Same as whmcsMappings since sfAccountId is required
        orphanedMappings: 0, // No longer possible
      };
      this.logger.debug("Generated mapping statistics", stats);
      return stats;
    } catch (error) {
      this.logger.error("Failed to get mapping statistics", { error: extractErrorMessage(error) });
      throw error;
    }
  }

  async hasMapping(userId: string): Promise<boolean> {
    try {
      const cached = await this.cacheService.getByUserId(userId);
      if (cached) return true;
      return this.idMappingRepository.exists({ userId });
    } catch (error) {
      this.logger.error(`Failed to check mapping for user ${userId}`, {
        error: extractErrorMessage(error),
      });
      return false;
    }
  }

  /**
   * Get WHMCS client ID for a user, throwing if not found.
   * Use this when WHMCS client ID is required for an operation.
   *
   * @throws NotFoundException if mapping or WHMCS client ID not found
   */
  async getWhmcsClientIdOrThrow(userId: string): Promise<number> {
    const mapping = await this.findByUserId(userId);
    if (!mapping?.whmcsClientId) {
      throw new NotFoundException("WHMCS client mapping not found");
    }
    return mapping.whmcsClientId;
  }

  /**
   * Get full mapping for a user, throwing if not found.
   * Use when you need both whmcsClientId and sfAccountId.
   *
   * @throws NotFoundException if mapping not found
   */
  async getMappingOrThrow(userId: string): Promise<UserIdMapping> {
    const mapping = await this.findByUserId(userId);
    if (!mapping) {
      throw new NotFoundException("User mapping not found");
    }
    return mapping;
  }

  async invalidateCache(userId: string): Promise<void> {
    const mapping = await this.cacheService.getByUserId(userId);
    if (mapping) {
      await this.cacheService.deleteMapping(mapping);
    }
    this.logger.log(`Invalidated mapping cache for user ${userId}`);
  }

  private sanitizeForLog(data: unknown): Record<string, unknown> {
    try {
      const plain: unknown = JSON.parse(JSON.stringify(data ?? {}));
      if (plain && typeof plain === "object" && !Array.isArray(plain)) {
        return plain as Record<string, unknown>;
      }
      return { value: plain } as Record<string, unknown>;
    } catch {
      return { value: String(data) } as Record<string, unknown>;
    }
  }
}
