import { Inject, Injectable } from '@nestjs/common';
import { POIType } from '@prisma/client';
import { PrismaService, LoggerService } from '@modules/shared';
import {
  type INeighborhoodScoreService,
  type NeighborhoodScoreResult,
} from '../../domain/services/neighborhood-score.service';
import {
  AI_SERVICE_CLIENT,
  type AiNeighborhoodPOICounts,
  type IAiServiceClient,
} from './ai-service.client';

/**
 * Scoring weights for each POI category.
 * Sum = 100 (total score is 0–100 weighted average).
 * Mirrors the Python heuristic in libs/ai-services/app/services/neighborhood_service.py.
 */
const CATEGORY_WEIGHTS = {
  education: 20,
  healthcare: 20,
  transport: 20,
  shopping: 15,
  greenery: 15,
  safety: 10,
} as const;

/** POI types grouped by scoring category. */
const CATEGORY_POI_TYPES: Record<keyof typeof CATEGORY_WEIGHTS, POIType[]> = {
  education: [POIType.SCHOOL, POIType.UNIVERSITY],
  healthcare: [POIType.HOSPITAL, POIType.CLINIC],
  transport: [POIType.METRO_STATION, POIType.BUS_STOP],
  shopping: [POIType.MALL, POIType.MARKET, POIType.SUPERMARKET],
  greenery: [POIType.PARK],
  safety: [POIType.POLICE_STATION, POIType.FIRE_STATION],
};

/** Max count per category that yields a 10/10 score. */
const MAX_COUNTS: Record<keyof typeof CATEGORY_WEIGHTS, number> = {
  education: 15,
  healthcare: 8,
  transport: 12,
  shopping: 10,
  greenery: 6,
  safety: 4,
};

type CategoryKey = keyof typeof CATEGORY_WEIGHTS;
const CATEGORY_KEYS = Object.keys(CATEGORY_WEIGHTS) as CategoryKey[];

@Injectable()
export class PrismaNeighborhoodScoreService implements INeighborhoodScoreService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly logger: LoggerService,
  ) {}

  async getScore(district: string, city: string): Promise<NeighborhoodScoreResult | null> {
    const existing = await this.prisma.neighborhoodScore.findUnique({
      where: { district_city: { district, city } },
    });

    if (!existing) return null;

    return mapRecord(existing);
  }

  async calculateAndSave(district: string, city: string): Promise<NeighborhoodScoreResult> {
    const counts = await countPOIs(this.prisma, district, city);
    const subScores = scoreFromCounts(counts);
    const totalScore = weightedTotal(subScores);

    const result = await upsertScore(this.prisma, district, city, subScores, totalScore, counts);
    this.logger.log(
      `Neighborhood score (prisma) calculated: ${district}, ${city} → total=${result.totalScore}`,
      'NeighborhoodScoreService',
    );
    return mapRecord(result);
  }
}

/**
 * Calls the Python AI service to compute scores; falls back to local Prisma scoring
 * when the service is unavailable or the call times out. Persists to NeighborhoodScore.
 */
@Injectable()
export class HttpNeighborhoodScoreService implements INeighborhoodScoreService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly logger: LoggerService,
    @Inject(AI_SERVICE_CLIENT) private readonly aiClient: IAiServiceClient,
    private readonly fallback: PrismaNeighborhoodScoreService,
  ) {}

  async getScore(district: string, city: string): Promise<NeighborhoodScoreResult | null> {
    return this.fallback.getScore(district, city);
  }

  async calculateAndSave(district: string, city: string): Promise<NeighborhoodScoreResult> {
    const counts = await countPOIs(this.prisma, district, city);

    try {
      const aiResult = await this.aiClient.scoreNeighborhood({
        district,
        city,
        poi_counts: counts,
      });

      const subScores: Record<CategoryKey, number> = {
        education: aiResult.education_score,
        healthcare: aiResult.healthcare_score,
        transport: aiResult.transport_score,
        shopping: aiResult.shopping_score,
        greenery: aiResult.greenery_score,
        safety: aiResult.safety_score,
      };

      const result = await upsertScore(
        this.prisma,
        district,
        city,
        subScores,
        aiResult.total_score,
        counts,
      );
      this.logger.log(
        `Neighborhood score (ai=${aiResult.algorithm_version}): ${district}, ${city} → total=${result.totalScore}`,
        'NeighborhoodScoreService',
      );
      return mapRecord(result);
    } catch (err) {
      this.logger.warn(
        `AI neighborhood score unavailable, falling back to prisma scoring: ${(err as Error).message}`,
        'NeighborhoodScoreService',
      );
      return this.fallback.calculateAndSave(district, city);
    }
  }
}

async function countPOIs(
  prisma: PrismaService,
  district: string,
  city: string,
): Promise<AiNeighborhoodPOICounts> {
  const entries = await Promise.all(
    CATEGORY_KEYS.map(async (cat) => {
      const count = await prisma.pOI.count({
        where: {
          district,
          city,
          type: { in: CATEGORY_POI_TYPES[cat] },
        },
      });
      return [cat, count] as const;
    }),
  );

  return Object.fromEntries(entries) as unknown as AiNeighborhoodPOICounts;
}

function scoreFromCounts(counts: AiNeighborhoodPOICounts): Record<CategoryKey, number> {
  return Object.fromEntries(
    CATEGORY_KEYS.map((cat) => {
      const raw = counts[cat] ?? 0;
      const max = MAX_COUNTS[cat];
      return [cat, Math.min(10, (raw / max) * 10)];
    }),
  ) as Record<CategoryKey, number>;
}

function weightedTotal(subScores: Record<CategoryKey, number>): number {
  const sum = CATEGORY_KEYS.reduce(
    (acc, cat) => acc + (subScores[cat] * CATEGORY_WEIGHTS[cat]) / 10,
    0,
  );
  return Math.round(sum * 10) / 10;
}

async function upsertScore(
  prisma: PrismaService,
  district: string,
  city: string,
  subScores: Record<CategoryKey, number>,
  totalScore: number,
  counts: AiNeighborhoodPOICounts,
) {
  const calculatedAt = new Date();
  const data = {
    educationScore: subScores.education,
    healthcareScore: subScores.healthcare,
    transportScore: subScores.transport,
    shoppingScore: subScores.shopping,
    greeneryScore: subScores.greenery,
    safetyScore: subScores.safety,
    totalScore,
    poiCounts: counts as unknown as Record<string, number>,
    calculatedAt,
  };

  return prisma.neighborhoodScore.upsert({
    where: { district_city: { district, city } },
    create: { district, city, ...data },
    update: data,
  });
}

function mapRecord(record: {
  district: string;
  city: string;
  educationScore: number;
  healthcareScore: number;
  transportScore: number;
  shoppingScore: number;
  greeneryScore: number;
  safetyScore: number;
  totalScore: number;
  poiCounts: unknown;
  calculatedAt: Date;
}): NeighborhoodScoreResult {
  return {
    district: record.district,
    city: record.city,
    educationScore: record.educationScore,
    healthcareScore: record.healthcareScore,
    transportScore: record.transportScore,
    shoppingScore: record.shoppingScore,
    greeneryScore: record.greeneryScore,
    safetyScore: record.safetyScore,
    totalScore: record.totalScore,
    poiCounts: record.poiCounts as Record<string, number>,
    calculatedAt: record.calculatedAt,
  };
}

/**
 * @deprecated Use HttpNeighborhoodScoreService (binds AI proxy + prisma fallback).
 * Kept exported for backward compatibility with callers/tests.
 */
export { PrismaNeighborhoodScoreService as NeighborhoodScoreServiceImpl };
