import {
  Injectable,
  BadRequestException,
  ForbiddenException,
  NotFoundException,
} from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import { ImageService } from '@/utils/image/image.service';
import { PushService } from '../push/push.service';
import {
  containsDisallowedUserContent,
  MODERATION_REJECTION_MESSAGE,
} from '@/utils/content-moderation';

const DEFAULT_RADIUS_KM = 10;
const PET_CLASSIFIER_URL =
  process.env.PET_CLASSIFIER_URL || 'http://localhost:8012/detect-pet-multi';
const PET_CLASSIFIER_TIMEOUT_MS = 15_000;

type BreedDetectionResult = {
  pet_type?: string;
  breed?: string;
  size?: string;
  color?: string;
  confidence?: number;
  supported?: boolean;
  is_animal?: boolean;
  detection_reason?: string;
  not_animal_reason?: string;
  detected_object?: string;
  detected_object_confidence?: number;
  detected_objects?: unknown[];
  images_processed?: number;
};

type QuickSnapFlag = {
  status: 'clear' | 'flagged';
  reason: string;
  source: 'system' | null;
  payload: Record<string, unknown>;
};

function parseNumber(value: any, fallback = 0) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function normalizePetType(type?: string): string {
  return type ? type.toLowerCase() : '';
}

function normalizeClassifierValue(value: unknown): string {
  return typeof value === 'string' ? value.trim() : '';
}

function normalizeColor(value: unknown): string {
  if (Array.isArray(value)) {
    return value
      .filter((v) => typeof v === 'string')
      .join(', ')
      .trim();
  }
  return normalizeClassifierValue(value);
}

function parseConfidence(value: unknown): number | undefined {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function buildNonAnimalFlag(result: any): QuickSnapFlag | null {
  const petType = normalizeClassifierValue(result?.pet_type).toLowerCase();
  const supported = result?.supported === true;
  const isAnimal = result?.is_animal === true;
  const isExplicitNonAnimal =
    petType === 'not_animal' ||
    result?.is_animal === false ||
    normalizeClassifierValue(result?.not_animal_reason) ||
    normalizeClassifierValue(result?.detected_object);

  if (supported || isAnimal || !isExplicitNonAnimal) return null;

  const detectedObject = normalizeClassifierValue(result?.detected_object);
  const reason =
    normalizeClassifierValue(result?.not_animal_reason) ||
    normalizeClassifierValue(result?.detection_reason) ||
    'non_animal_object_detected';

  return {
    status: 'flagged',
    reason,
    source: 'system',
    payload: {
      pet_type: petType || 'not_animal',
      supported: false,
      is_animal: false,
      detection_reason: normalizeClassifierValue(result?.detection_reason) || reason,
      not_animal_reason: reason,
      detected_object: detectedObject || null,
      detected_object_confidence: parseConfidence(result?.detected_object_confidence) ?? null,
      detected_objects: Array.isArray(result?.detected_objects) ? result.detected_objects : [],
      images_processed: parseConfidence(result?.images_processed) ?? null,
    },
  };
}

function buildClearFlag(): QuickSnapFlag {
  return { status: 'clear', reason: '', source: null, payload: {} };
}

@Injectable()
export class QuickSnapsService {
  constructor(
    private readonly db: Kysely<DB>,
    private readonly logger: Logger,
    private readonly imageService: ImageService,
    private readonly pushService: PushService,
  ) {}

  private async insertFlaggedReport(input: {
    targetType: 'quick_snap' | 'report' | 'report_form';
    sourceTable?: string | null;
    targetId?: string | null;
    userId?: string | null;
    flagStatus?: 'flagged' | 'disputed';
    flagSource?: 'system' | 'user';
    systemReason?: string | null;
    userReason?: string | null;
    payload?: Record<string, unknown>;
    client?: Kysely<DB>;
  }): Promise<string> {
    const payload = input.payload || {};
    const db = input.client || this.db;
    const result = await sql<{ id: string }>`
      INSERT INTO flagged_reports (
        target_type, source_table, target_id, user_id, flag_status, flag_source,
        system_reason, user_reason, detected_object, detected_object_confidence,
        detection_payload, review_status
      ) VALUES (
        ${input.targetType}, ${input.sourceTable || input.targetType}, ${input.targetId || null},
        ${input.userId || null}, ${input.flagStatus || 'flagged'}, ${input.flagSource || 'system'},
        ${input.systemReason || null}, ${input.userReason || null},
        ${typeof payload.detected_object === 'string' ? payload.detected_object : null},
        ${parseConfidence(payload.detected_object_confidence) ?? null},
        ${JSON.stringify(payload)}, 'open'
      ) RETURNING id
    `.execute(db);
    return String(result.rows[0]?.id || '');
  }

  private async detectPetClassification(
    photoFile: Express.Multer.File | undefined,
  ): Promise<BreedDetectionResult | null> {
    if (!photoFile?.buffer) return null;

    const form = new FormData();
    const fileBlob = new Blob([new Uint8Array(photoFile.buffer)], {
      type: photoFile.mimetype || 'image/jpeg',
    });
    form.append('files', fileBlob, photoFile.originalname || 'quick-snap.jpg');

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), PET_CLASSIFIER_TIMEOUT_MS);

    try {
      const response = await fetch(PET_CLASSIFIER_URL, {
        method: 'POST',
        body: form,
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error(`Classifier request failed with status ${response.status}`);
      }

      const result = (await response.json()) as any;
      const nonAnimalFlag = buildNonAnimalFlag(result);
      if (nonAnimalFlag) {
        return {
          supported: false,
          is_animal: false,
          detection_reason: String(nonAnimalFlag.payload.detection_reason || ''),
          not_animal_reason: String(nonAnimalFlag.payload.not_animal_reason || ''),
          detected_object: String(nonAnimalFlag.payload.detected_object || ''),
          detected_object_confidence:
            parseConfidence(nonAnimalFlag.payload.detected_object_confidence) ?? undefined,
          detected_objects: nonAnimalFlag.payload.detected_objects as unknown[],
          images_processed: parseConfidence(nonAnimalFlag.payload.images_processed),
        };
      }

      if (!result?.supported) {
        return {
          supported: false,
          detection_reason:
            normalizeClassifierValue(result?.detection_reason) || 'unsupported_detection',
        };
      }

      return {
        supported: true,
        is_animal: true,
        pet_type: normalizePetType(normalizeClassifierValue(result.pet_type)),
        breed: normalizeClassifierValue(result.breed),
        size: normalizeClassifierValue(result.size).toLowerCase(),
        color: normalizeColor(result.color),
        confidence: typeof result.confidence === 'number' ? result.confidence : undefined,
      };
    } finally {
      clearTimeout(timeout);
    }
  }

  async createQuickSnap(
    user_id: string,
    data: any,
    files?: { photo?: Express.Multer.File[] },
  ) {
    const {
      photo_uri: bodyPhotoUri,
      device_id: sourceDeviceId,
      pet_type = '',
      breed = '',
      size = '',
      color = '',
      latitude,
      longitude,
      location_name = '',
      visibility = 'story',
    } = data || {};

    if (containsDisallowedUserContent([pet_type, breed, size, color, location_name])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    let photoUri = bodyPhotoUri || '';
    const snapPhoto = files?.photo?.[0];
    let detected: BreedDetectionResult | null = null;
    let classifierFailed = false;
    try {
      detected = await this.detectPetClassification(snapPhoto);
    } catch (error) {
      this.logger.error('[QuickSnap] Classifier failed', error);
      classifierFailed = true;
    }

    const uploadedUrls = await this.imageService.uploadMulterFiles(files?.photo ?? [], 'quick-snaps');
    if (uploadedUrls.length > 0) {
      photoUri = uploadedUrls[0];
    }

    if (!photoUri) {
      throw new BadRequestException('Photo is required');
    }

    const lat = parseNumber(latitude, 0);
    const lng = parseNumber(longitude, 0);
    const radiusKm = DEFAULT_RADIUS_KM;
    const flag =
      detected?.is_animal === false ? buildNonAnimalFlag(detected) || buildClearFlag() : buildClearFlag();
    const canUseDetection = !classifierFailed && detected?.supported !== false;
    const finalPetType = canUseDetection ? detected?.pet_type || normalizePetType(pet_type) : '';
    const finalBreed = canUseDetection ? detected?.breed || breed || '' : '';
    const finalSize = canUseDetection ? detected?.size || String(size || '').toLowerCase() : '';
    const finalColor = canUseDetection ? detected?.color || color || '' : '';

    const snapInsert = await sql<any>`
      INSERT INTO quick_snaps (
        user_id, image_url, photo_uri, type, pet_type, breed, size, color, latitude, longitude, location_name, visibility
      ) VALUES (
        ${user_id}, ${photoUri}, ${photoUri}, 'quick_snap', ${finalPetType}, ${finalBreed},
        ${finalSize}, ${finalColor}, ${lat}, ${lng}, ${location_name || ''}, ${visibility || 'story'}
      ) RETURNING *
    `.execute(this.db);

    let savedSnap = snapInsert.rows[0];

    if (flag.status === 'flagged') {
      const flagId = await this.insertFlaggedReport({
        targetType: 'quick_snap',
        sourceTable: 'quick_snaps',
        targetId: savedSnap.id,
        userId: user_id,
        systemReason: flag.reason,
        payload: flag.payload,
      });
      const flaggedSnap = await sql<any>`
        UPDATE quick_snaps SET is_flagged = true, flag_id = ${flagId}, updated_at = NOW()
        WHERE id = ${savedSnap.id} RETURNING *
      `.execute(this.db);
      savedSnap = flaggedSnap.rows[0] || savedSnap;
    }

    let notifiedUsers = 0;
    let notifiedDevices = 0;
    if (lat !== 0 || lng !== 0) {
      const delta = radiusKm / 111.0;
      const nearbyDevicesResult = await sql<{
        device_id: string;
        user_id: string | null;
        expo_push_token: string | null;
      }>`
        SELECT d.device_id, d.user_id, d.expo_push_token
        FROM devices d
        WHERE d.deleted_at IS NULL
          AND d.expo_push_token IS NOT NULL
          AND d.latitude IS NOT NULL
          AND d.longitude IS NOT NULL
          AND (d.user_id IS NULL OR d.user_id <> ${user_id})
          AND NOT EXISTS (
            SELECT 1 FROM blocked_users bu
            WHERE bu.deleted_at IS NULL AND d.user_id IS NOT NULL
              AND (
                (bu.blocker_id = d.user_id AND bu.blocked_id = ${user_id})
                OR (bu.blocker_id = ${user_id} AND bu.blocked_id = d.user_id)
              )
          )
          AND (${sourceDeviceId ? String(sourceDeviceId) : null}::text IS NULL OR d.device_id <> ${sourceDeviceId ? String(sourceDeviceId) : null}::text)
          AND d.latitude BETWEEN ${lat - delta} AND ${lat + delta}
          AND d.longitude BETWEEN ${lng - delta} AND ${lng + delta}
          AND (
            6371 * acos(
              GREATEST(-1.0, LEAST(1.0,
                cos(radians(${lat})) * cos(radians(d.latitude)) * cos(radians(d.longitude) - radians(${lng})) +
                sin(radians(${lat})) * sin(radians(d.latitude))
              ))
            )
          ) <= ${radiusKm}
      `.execute(this.db);

      const title = 'Quick snap nearby';
      const body = 'A pet snap was shared near you.';
      const notifiedUserSet = new Set<string>();
      const notifiedTokenSet = new Set<string>();

      for (const row of nearbyDevicesResult.rows) {
        const pushToken = String(row.expo_push_token || '').trim();
        if (!pushToken || notifiedTokenSet.has(pushToken)) continue;
        notifiedTokenSet.add(pushToken);
        notifiedDevices += 1;

        const targetUserId = row.user_id ? String(row.user_id) : '';
        if (targetUserId && !notifiedUserSet.has(targetUserId)) {
          await sql`
            INSERT INTO notifications (user_id, type, title, message, report_id, profile_id, quick_snap_id)
            VALUES (${targetUserId}, 'quick_snap', ${title}, ${body}, NULL, NULL, ${savedSnap.id})
          `.execute(this.db);
          notifiedUserSet.add(targetUserId);
          notifiedUsers += 1;
        }

        await this.pushService.sendPushNotification(pushToken, targetUserId || user_id, title, body, {
          type: 'quick_snap_nearby',
          quick_snap_id: savedSnap.id,
        });
      }
    }

    return {
      status: true,
      statusCode: 201,
      data: { snap: savedSnap, notified_users: notifiedUsers, notified_devices: notifiedDevices, radius_km: radiusKm },
    };
  }

  async getQuickSnapById(user_id: string, id: string) {
    const result = await sql<any>`
      SELECT
        qs.id, qs.user_id, qs.image_url, qs.photo_uri, qs.pet_type, qs.breed, qs.size, qs.color,
        qs.is_flagged, qs.flag_id,
        fr.detection_payload AS flag_payload, fr.system_reason AS flag_reason,
        EXISTS (
          SELECT 1 FROM flagged_reports dispute
          WHERE dispute.deleted_at IS NULL AND dispute.target_type = 'quick_snap'
            AND dispute.target_id = qs.id AND dispute.flag_status = 'disputed'
        ) AS flag_disputed,
        qs.location_name, qs.latitude, qs.longitude, qs.visibility, qs.created_at,
        u.display_name AS profile_name
      FROM quick_snaps qs
      JOIN users u ON u.id = qs.user_id AND u.deleted_at IS NULL
      LEFT JOIN flagged_reports fr ON fr.id = qs.flag_id AND fr.deleted_at IS NULL
      WHERE qs.id = ${id} AND qs.deleted_at IS NULL
        AND NOT EXISTS (
          SELECT 1 FROM blocked_users bu
          WHERE bu.blocker_id = ${user_id} AND bu.blocked_id = qs.user_id AND bu.deleted_at IS NULL
        )
      LIMIT 1
    `.execute(this.db);

    if (result.rows.length === 0) {
      throw new NotFoundException('Quick snap not found');
    }

    const snap = result.rows[0];
    const canView = snap.visibility !== 'private' || snap.user_id === user_id;
    if (!canView) {
      throw new ForbiddenException('Not authorized to view this quick snap');
    }

    return { status: true, statusCode: 200, data: snap };
  }

  async getMyQuickSnaps(user_id: string) {
    const result = await sql<any>`
      SELECT
        qs.id, qs.image_url, qs.photo_uri, qs.pet_type, qs.breed, qs.color, qs.is_flagged, qs.flag_id,
        fr.detection_payload AS flag_payload, fr.system_reason AS flag_reason,
        EXISTS (
          SELECT 1 FROM flagged_reports dispute
          WHERE dispute.deleted_at IS NULL AND dispute.target_type = 'quick_snap'
            AND dispute.target_id = qs.id AND dispute.flag_status = 'disputed'
        ) AS flag_disputed,
        qs.location_name, qs.created_at
      FROM quick_snaps qs
      LEFT JOIN flagged_reports fr ON fr.id = qs.flag_id AND fr.deleted_at IS NULL
      WHERE qs.user_id = ${user_id} AND qs.deleted_at IS NULL
      ORDER BY qs.created_at DESC
      LIMIT 200
    `.execute(this.db);

    return { status: true, statusCode: 200, data: result.rows };
  }

  async disputeQuickSnapFlag(user_id: string, id: string, reason: string) {
    const cleanedReason = String(reason || '').trim();
    if (!cleanedReason) {
      throw new BadRequestException('Reason is required');
    }
    if (containsDisallowedUserContent([cleanedReason])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    const snapResult = await sql<any>`
      SELECT qs.id, qs.user_id, qs.visibility, qs.is_flagged, qs.flag_id,
             fr.system_reason AS flag_reason, fr.detection_payload AS flag_payload
      FROM quick_snaps qs
      LEFT JOIN flagged_reports fr ON fr.id = qs.flag_id AND fr.deleted_at IS NULL
      WHERE qs.id = ${id} AND qs.deleted_at IS NULL
      LIMIT 1
    `.execute(this.db);

    if (snapResult.rows.length === 0) {
      throw new NotFoundException('Quick snap not found');
    }

    const snap = snapResult.rows[0];
    if (snap.visibility === 'private' && String(snap.user_id) !== user_id) {
      throw new ForbiddenException('Not authorized to report this quick snap flag');
    }
    if (!snap.is_flagged || !snap.flag_id) {
      throw new BadRequestException('This quick snap is not flagged');
    }

    await this.insertFlaggedReport({
      targetType: 'quick_snap',
      sourceTable: 'quick_snaps',
      targetId: id,
      userId: user_id,
      flagStatus: 'disputed',
      flagSource: 'user',
      systemReason: snap.flag_reason || null,
      userReason: cleanedReason,
      payload: snap.flag_payload || {},
    });

    return { status: true, statusCode: 201, data: { message: 'Flag dispute submitted' } };
  }

  async deleteQuickSnapById(user_id: string, id: string) {
    const ownDelete = await sql<{ id: string }>`
      UPDATE quick_snaps SET deleted_at = NOW()
      WHERE id = ${id} AND user_id = ${user_id} AND deleted_at IS NULL
      RETURNING id
    `.execute(this.db);

    if (ownDelete.rows.length > 0) {
      return { status: true, statusCode: 200, data: { id, message: 'Quick snap deleted' } };
    }

    const exists = await sql<{ id: string }>`
      SELECT id, user_id FROM quick_snaps WHERE id = ${id} AND deleted_at IS NULL LIMIT 1
    `.execute(this.db);

    if (exists.rows.length === 0) {
      throw new NotFoundException('Quick snap not found');
    }

    throw new ForbiddenException('Not authorized to delete this quick snap');
  }
}
