import {
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../common/prisma/prisma.service';
import { buildRouteCacheKey } from '@vihar/db';

export interface WalkingRoute {
  distanceMeters: number;
  durationSeconds: number;
  polyline?: string;
  cached: boolean;
}

@Injectable()
export class RoutesService {
  private readonly logger = new Logger(RoutesService.name);
  private readonly apiKey: string;
  private readonly cacheTtlDays: number;

  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService,
  ) {
    this.apiKey = this.config.get<string>('GOOGLE_MAPS_BACKEND_KEY') ?? '';
    this.cacheTtlDays = parseInt(this.config.get<string>('ROUTE_CACHE_TTL_DAYS') ?? '180', 10);
  }

  /**
   * Walking distance between two locations (by their internal location IDs).
   * Always checks cache first; only hits Google when miss.
   */
  async getWalkingByLocationIds(
    cityId: number,
    fromLocationId: number,
    toLocationId: number,
  ): Promise<WalkingRoute> {
    const [from, to] = await Promise.all([
      this.prisma.location.findUnique({ where: { locationId: fromLocationId } }),
      this.prisma.location.findUnique({ where: { locationId: toLocationId } }),
    ]);
    if (!from || !to || from.cityId !== cityId || to.cityId !== cityId) {
      throw new NotFoundException('One or both locations not found in this city');
    }
    if (!from.googlePlaceId || !to.googlePlaceId) {
      throw new InternalServerErrorException(
        'Locations missing google_place_id - cannot compute walking route',
      );
    }
    return this.getWalkingByPlaceIds(from.googlePlaceId, to.googlePlaceId);
  }

  /**
   * Walking distance between two place_ids. Cached by sorted-pair key.
   */
  async getWalkingByPlaceIds(
    originPlaceId: string,
    destinationPlaceId: string,
  ): Promise<WalkingRoute> {
    const key = buildRouteCacheKey(originPlaceId, destinationPlaceId);

    // 1. Cache check
    const cached = await this.prisma.routeDistanceCache.findUnique({ where: { cacheKey: key } });
    if (cached && cached.expiresAt > new Date()) {
      return {
        distanceMeters: cached.distanceMeters,
        durationSeconds: cached.durationSeconds,
        polyline: cached.polyline ?? undefined,
        cached: true,
      };
    }

    // 2. Miss -> fetch from Google
    if (!this.apiKey) {
      throw new InternalServerErrorException('Google Maps key not configured');
    }
    const fresh = await this.fetchFromGoogle(originPlaceId, destinationPlaceId);

    // 3. Persist
    const expiresAt = new Date(Date.now() + this.cacheTtlDays * 86400_000);
    await this.prisma.routeDistanceCache.upsert({
      where: { cacheKey: key },
      update: {
        distanceMeters: fresh.distanceMeters,
        durationSeconds: fresh.durationSeconds,
        polyline: fresh.polyline ?? null,
        fetchedAt: new Date(),
        expiresAt,
      },
      create: {
        cacheKey: key,
        originPlaceId,
        destinationPlaceId,
        distanceMeters: fresh.distanceMeters,
        durationSeconds: fresh.durationSeconds,
        polyline: fresh.polyline ?? null,
        expiresAt,
      },
    });

    return { ...fresh, cached: false };
  }

  private async fetchFromGoogle(
    originPlaceId: string,
    destinationPlaceId: string,
  ): Promise<Omit<WalkingRoute, 'cached'>> {
    let res: Response;
    try {
      res = await fetch('https://routes.googleapis.com/directions/v2:computeRoutes', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Goog-Api-Key': this.apiKey,
          'X-Goog-FieldMask':
            'routes.distanceMeters,routes.duration,routes.polyline.encodedPolyline',
        },
        body: JSON.stringify({
          origin: { placeId: originPlaceId },
          destination: { placeId: destinationPlaceId },
          travelMode: 'WALK',
          polylineEncoding: 'ENCODED_POLYLINE',
          units: 'METRIC',
        }),
      });
    } catch (e) {
      this.logger.error('Routes API network error', e instanceof Error ? e.message : String(e));
      throw new InternalServerErrorException('Failed to reach Google Routes API');
    }

    if (!res.ok) {
      const body = await res.text().catch(() => '');
      this.logger.error(`Routes API HTTP ${res.status}: ${body.slice(0, 200)}`);
      throw new InternalServerErrorException(`Google Routes API error (${res.status})`);
    }

    const json = await res.json() as { routes?: any[] };
    const route = json.routes?.[0];
    if (!route) {
      throw new NotFoundException('No walking route found between these locations');
    }

    return {
      distanceMeters: route.distanceMeters as number,
      durationSeconds: parseInt(String(route.duration).replace('s', ''), 10),
      polyline: route.polyline?.encodedPolyline as string | undefined,
    };
  }
}
