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

@Injectable()
export class MappingsService {
  constructor(
    private readonly prisma: PrismaService,
    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.prisma.idMapping.findUnique({ where: { userId: sanitizedRequest.userId } }),
        this.prisma.idMapping.findUnique({
          where: { whmcsClientId: sanitizedRequest.whmcsClientId },
        }),
        sanitizedRequest.sfAccountId
          ? this.prisma.idMapping.findFirst({
              where: { 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 = [...checkMappingCompleteness(sanitizedRequest), ...conflictCheck.warnings];

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

      let created;
      try {
        created = await this.prisma.idMapping.create({ data: sanitizedRequest });
      } catch (e) {
        const msg = getErrorMessage(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: getErrorMessage(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.prisma.idMapping.findFirst({ where: { 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: getErrorMessage(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.prisma.idMapping.findUnique({ where: { 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: getErrorMessage(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.prisma.idMapping.findUnique({ where: { 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: getErrorMessage(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 updated = await this.prisma.idMapping.update({
        where: { userId },
        data: sanitizedUpdates,
      });

      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: getErrorMessage(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.prisma.idMapping.delete({ where: { 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: getErrorMessage(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 } };
        }
      }
      if (filters.hasSfMapping !== undefined) {
        whereClause.sfAccountId = filters.hasSfMapping ? { not: null } : { equals: null };
      }

      const dbMappings = await this.prisma.idMapping.findMany({
        where: 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: getErrorMessage(error), filters });
      throw error;
    }
  }

  async getMappingStats(): Promise<MappingStats> {
    try {
      const [totalCount, whmcsCount, sfCount, completeCount] = await Promise.all([
        this.prisma.idMapping.count(),
        this.prisma.idMapping.count({ where: { whmcsClientId: { gt: 0 } } }),
        this.prisma.idMapping.count({ where: { sfAccountId: { not: null } } }),
        this.prisma.idMapping.count({
          where: { whmcsClientId: { gt: 0 }, sfAccountId: { not: null } },
        }),
      ]);

      const orphanedMappings = whmcsCount - completeCount;

      const stats: MappingStats = {
        totalMappings: totalCount,
        whmcsMappings: whmcsCount,
        salesforceMappings: sfCount,
        completeMappings: completeCount,
        orphanedMappings: orphanedMappings < 0 ? 0 : orphanedMappings,
      };
      this.logger.debug("Generated mapping statistics", stats);
      return stats;
    } catch (error) {
      this.logger.error("Failed to get mapping statistics", { error: getErrorMessage(error) });
      throw error;
    }
  }

  async hasMapping(userId: string): Promise<boolean> {
    try {
      const cached = await this.cacheService.getByUserId(userId);
      if (cached) return true;
      const mapping = await this.prisma.idMapping.findUnique({
        where: { userId },
        select: { userId: true },
      });
      return mapping !== null;
    } catch (error) {
      this.logger.error(`Failed to check mapping for user ${userId}`, {
        error: getErrorMessage(error),
      });
      return false;
    }
  }

  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>;
    }
  }
}
