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

const PET_TOKEN_REGEX = /^\[\[pet:([0-9a-fA-F-]{36})\]\]$/;
const REPORT_TOKEN_REGEX = /^\[\[report:([0-9a-fA-F-]{36})\]\]$/;
const LOCATION_TOKEN_REGEX =
  /^\[\[location:([-+]?\d+(?:\.\d+)?),([-+]?\d+(?:\.\d+)?)(?:\|(.+))?\]\]$/;

type ConversationRow = {
  id: string;
  report_id: string | null;
  participant1_id: string;
  participant2_id: string;
  participant1Name: string;
  participant2Name: string;
  last_message_text: string;
  last_message_at: string;
  created_at: string;
  unreadCount: number;
  reportpet_name: string | null;
  reportStatus: string | null;
  reportPhoto: string | null;
};

type MessageRow = {
  id: string;
  conversationId: string;
  senderId: string;
  senderName: string;
  text: string;
  readAt: string | null;
  created_at: string;
};

type OpenConversationInput = { otherUserId: string; report_id?: string | null };

type ParsedMessageContent = {
  messageType: 'text' | 'pet_token' | 'report_token' | 'location_token';
  petProfileId: string | null;
  reportId: string | null;
  location: { latitude: number; longitude: number; label: string } | null;
  previewText: string;
};

function safeJsonArray(value: any): string[] {
  if (Array.isArray(value)) return value.filter(Boolean).map((x) => String(x));
  if (typeof value === 'string') {
    try {
      const parsed = JSON.parse(value);
      return Array.isArray(parsed) ? parsed.filter(Boolean).map((x) => String(x)) : [];
    } catch {
      return [];
    }
  }
  return [];
}

export function parseMessageContent(text: string): ParsedMessageContent {
  const value = String(text || '').trim();

  const petMatch = value.match(PET_TOKEN_REGEX);
  if (petMatch?.[1]) {
    return {
      messageType: 'pet_token',
      petProfileId: petMatch[1],
      reportId: null,
      location: null,
      previewText: 'Shared a pet',
    };
  }

  const reportMatch = value.match(REPORT_TOKEN_REGEX);
  if (reportMatch?.[1]) {
    return {
      messageType: 'report_token',
      petProfileId: null,
      reportId: reportMatch[1],
      location: null,
      previewText: 'Shared a report',
    };
  }

  const locationMatch = value.match(LOCATION_TOKEN_REGEX);
  if (locationMatch?.[1] && locationMatch?.[2]) {
    const latitude = Number(locationMatch[1]);
    const longitude = Number(locationMatch[2]);
    const encodedLabel = locationMatch[3] ? String(locationMatch[3]) : '';
    let label = 'Shared location';
    if (encodedLabel) {
      try {
        label = decodeURIComponent(encodedLabel);
      } catch {
        label = encodedLabel;
      }
    }
    return {
      messageType: 'location_token',
      petProfileId: null,
      reportId: null,
      location: { latitude, longitude, label },
      previewText: 'Shared location',
    };
  }

  return {
    messageType: 'text',
    petProfileId: null,
    reportId: null,
    location: null,
    previewText: value,
  };
}

function normalizeConversationRow(row: ConversationRow, currentUserId: string) {
  const otherUserId =
    row.participant1_id === currentUserId ? row.participant2_id : row.participant1_id;
  const otherUserName =
    row.participant1_id === currentUserId ? row.participant2Name : row.participant1Name;
  const parsed = parseMessageContent(row.last_message_text || '');

  return {
    id: row.id,
    report_id: row.report_id,
    otherUserId,
    otherUserName,
    lastMessageText: parsed.previewText,
    lastMessageRawText: row.last_message_text || '',
    lastMessageType: parsed.messageType,
    lastMessagePetProfileId: parsed.petProfileId,
    lastMessageReportId: parsed.reportId,
    lastMessageLocation: parsed.location,
    lastMessageAt: row.last_message_at,
    unreadCount: Number(row.unreadCount || 0),
    reportpet_name: row.reportpet_name,
    reportStatus: row.reportStatus,
    reportPhoto: row.reportPhoto,
    created_at: row.created_at,
  };
}

function normalizeConversationInput(user_id: string, participant2Id: string) {
  const a = String(user_id);
  const b = String(participant2Id);
  return a.localeCompare(b) <= 0
    ? { participant1Id: a, participant2Id: b }
    : { participant1Id: b, participant2Id: a };
}

@Injectable()
export class ConversationsService {
  constructor(private readonly db: Kysely<DB>) {}

  private async findConversationByIdForUser(conversationId: string, user_id: string) {
    const result = await sql<any>`
      SELECT c.*,
             u1.display_name as "participant1Name",
             u2.display_name as "participant2Name",
             pr.pet_name as "reportpet_name",
             pr.status as "reportStatus",
             pr.photo_uri as "reportPhoto"
      FROM conversations c
      JOIN users u1 ON c.participant1_id = u1.id AND u1.deleted_at IS NULL
      JOIN users u2 ON c.participant2_id = u2.id AND u2.deleted_at IS NULL
      LEFT JOIN pet_reports pr ON c.report_id = pr.id AND pr.deleted_at IS NULL
      WHERE c.id = ${conversationId}
        AND (c.participant1_id = ${user_id} OR c.participant2_id = ${user_id})
        AND c.deleted_at IS NULL
        AND NOT EXISTS (
          SELECT 1 FROM blocked_users bu
          WHERE bu.deleted_at IS NULL
            AND (
              (bu.blocker_id = ${user_id} AND bu.blocked_id = CASE WHEN c.participant1_id = ${user_id} THEN c.participant2_id ELSE c.participant1_id END)
              OR
              (bu.blocked_id = ${user_id} AND bu.blocker_id = CASE WHEN c.participant1_id = ${user_id} THEN c.participant2_id ELSE c.participant1_id END)
            )
        )
      LIMIT 1
    `.execute(this.db);

    return result.rows[0] || null;
  }

  private validateMessageText(text: string): { ok: boolean; message?: string; value?: string } {
    const value = String(text || '').trim();
    if (!value) return { ok: false, message: 'Message text is required' };
    if (value.length > 2000) return { ok: false, message: 'Message text must be at most 2000 characters' };

    if (value.startsWith('[[') && value.endsWith(']]')) {
      const parsed = parseMessageContent(value);
      if (parsed.messageType === 'text') {
        return { ok: false, message: 'Invalid share token format' };
      }
      if (parsed.messageType === 'location_token') {
        const lat = parsed.location?.latitude;
        const lng = parsed.location?.longitude;
        if (
          !Number.isFinite(lat) ||
          !Number.isFinite(lng) ||
          lat! < -90 ||
          lat! > 90 ||
          lng! < -180 ||
          lng! > 180
        ) {
          return { ok: false, message: 'Invalid location token coordinates' };
        }
      }
    }

    if (parseMessageContent(value).messageType === 'text' && containsDisallowedUserContent([value])) {
      return { ok: false, message: MODERATION_REJECTION_MESSAGE };
    }

    return { ok: true, value };
  }

  private async buildMessageAttachments(rows: MessageRow[]) {
    const parsedByMessageId = new Map<string, ParsedMessageContent>();
    const petIds = new Set<string>();
    const reportIds = new Set<string>();

    rows.forEach((row) => {
      const parsed = parseMessageContent(row.text || '');
      parsedByMessageId.set(row.id, parsed);
      if (parsed.petProfileId) petIds.add(parsed.petProfileId);
      if (parsed.reportId) reportIds.add(parsed.reportId);
    });

    const petMap = new Map<string, any>();
    const reportMap = new Map<string, any>();

    if (petIds.size > 0) {
      const petResult = await sql<any>`
        SELECT pp.id, pp.user_id, pp.pet_type, pp.pet_name, pp.breed, pp.size, pp.color, pp.markings,
               pp.photo_uris, pp.suburb, pp.owner_name, pp.owner_phone, pp.created_at
        FROM pet_profiles pp
        WHERE pp.id = ANY(${Array.from(petIds)}::uuid[]) AND pp.deleted_at IS NULL
      `.execute(this.db);

      for (const row of petResult.rows) {
        petMap.set(String(row.id), {
          id: String(row.id),
          user_id: String(row.user_id),
          pet_type: row.pet_type,
          pet_name: row.pet_name,
          breed: row.breed,
          size: row.size,
          color: row.color,
          markings: row.markings,
          photo_uris: safeJsonArray(row.photo_uris),
          suburb: row.suburb,
          ownerName: row.owner_name,
          ownerPhone: row.owner_phone,
          created_at: row.created_at,
        });
      }
    }

    if (reportIds.size > 0) {
      const reportResult = await sql<any>`
        SELECT r.id, r.user_id, r.status, r.pet_type, r.pet_name, r.breed, r.size, r.color,
               r.photo_uri, r.photo_uris, r.location_name, r.latitude, r.longitude, r.created_at, r.is_reunited
        FROM pet_reports r
        WHERE r.id = ANY(${Array.from(reportIds)}::uuid[]) AND r.deleted_at IS NULL
      `.execute(this.db);

      for (const row of reportResult.rows) {
        reportMap.set(String(row.id), {
          id: String(row.id),
          user_id: String(row.user_id),
          status: row.status,
          pet_type: row.pet_type,
          pet_name: row.pet_name,
          breed: row.breed,
          size: row.size,
          color: row.color,
          location_name: row.location_name,
          latitude: Number(row.latitude),
          longitude: Number(row.longitude),
          photo_uri: row.photo_uri,
          photo_uris: safeJsonArray(row.photo_uris),
          created_at: row.created_at,
          is_reunited: Boolean(row.is_reunited),
        });
      }
    }

    return { parsedByMessageId, petMap, reportMap };
  }

  private normalizeMessageRow(
    row: MessageRow,
    parsed: ParsedMessageContent,
    currentUserId: string,
    pets: Map<string, any>,
    reports: Map<string, any>,
  ) {
    let attachment: any = null;

    if (parsed.messageType === 'pet_token' && parsed.petProfileId) {
      attachment = { type: 'pet', pet: pets.get(parsed.petProfileId) || null };
    }
    if (parsed.messageType === 'report_token' && parsed.reportId) {
      attachment = { type: 'report', report: reports.get(parsed.reportId) || null };
    }
    if (parsed.messageType === 'location_token' && parsed.location) {
      attachment = { type: 'location', location: parsed.location };
    }

    return {
      id: row.id,
      conversationId: row.conversationId,
      senderId: row.senderId,
      senderName: row.senderName,
      text: row.text,
      messageType: parsed.messageType,
      petProfileId: parsed.petProfileId,
      reportId: parsed.reportId,
      location: parsed.location,
      attachment,
      previewText: parsed.previewText,
      isMe: row.senderId === currentUserId,
      readAt: row.readAt,
      created_at: row.created_at,
    };
  }

  private async hydrateMessages(rows: MessageRow[], currentUserId: string) {
    const { parsedByMessageId, petMap, reportMap } = await this.buildMessageAttachments(rows);
    return rows.map((row) => {
      const parsed = parsedByMessageId.get(row.id) || parseMessageContent(row.text || '');
      return this.normalizeMessageRow(row, parsed, currentUserId, petMap, reportMap);
    });
  }

  async getConversations(user_id: string) {
    const result = await sql<ConversationRow>`
      SELECT c.*,
             u1.display_name as "participant1Name",
             u2.display_name as "participant2Name",
             pr.pet_name as "reportpet_name",
             pr.status as "reportStatus",
             pr.photo_uri as "reportPhoto",
             (
               SELECT COUNT(*)::int FROM messages m
               WHERE m.conversation_id = c.id AND m.sender_id != ${user_id} AND m.read_at IS NULL AND m.deleted_at IS NULL
             ) as "unreadCount"
      FROM conversations c
      JOIN users u1 ON c.participant1_id = u1.id AND u1.deleted_at IS NULL
      JOIN users u2 ON c.participant2_id = u2.id AND u2.deleted_at IS NULL
      LEFT JOIN pet_reports pr ON pr.id = c.report_id AND pr.deleted_at IS NULL
      WHERE (c.participant1_id = ${user_id} OR c.participant2_id = ${user_id})
        AND c.deleted_at IS NULL
        AND NOT EXISTS (
          SELECT 1 FROM blocked_users bu
          WHERE bu.deleted_at IS NULL
            AND (
              (bu.blocker_id = ${user_id} AND bu.blocked_id = CASE WHEN c.participant1_id = ${user_id} THEN c.participant2_id ELSE c.participant1_id END)
              OR
              (bu.blocked_id = ${user_id} AND bu.blocker_id = CASE WHEN c.participant1_id = ${user_id} THEN c.participant2_id ELSE c.participant1_id END)
            )
        )
      ORDER BY c.last_message_at DESC, c.created_at DESC
    `.execute(this.db);

    return {
      status: true,
      statusCode: 200,
      data: result.rows.map((row) => normalizeConversationRow(row, user_id)),
    };
  }

  async createConversation(user_id: string, data: { participant2Id: string; report_id?: string | null }) {
    return this.openConversation(user_id, {
      otherUserId: data.participant2Id,
      report_id: data.report_id || null,
    });
  }

  async openConversation(user_id: string, data: OpenConversationInput) {
    const otherUserId = String(data.otherUserId || '').trim();
    const reportId = data.report_id || null;

    if (!otherUserId) {
      throw new BadRequestException('otherUserId is required');
    }
    if (user_id === otherUserId) {
      throw new BadRequestException('You cannot start a conversation with yourself');
    }

    const userCheck = await sql<{ id: string; display_name: string }>`
      SELECT id, display_name FROM users WHERE id = ${otherUserId} AND deleted_at IS NULL LIMIT 1
    `.execute(this.db);
    if (userCheck.rows.length === 0) {
      throw new NotFoundException('User not found');
    }

    const blockCheck = await sql<{ id: string }>`
      SELECT id FROM blocked_users
      WHERE deleted_at IS NULL
        AND ((blocker_id = ${user_id} AND blocked_id = ${otherUserId}) OR (blocker_id = ${otherUserId} AND blocked_id = ${user_id}))
      LIMIT 1
    `.execute(this.db);
    if (blockCheck.rows.length > 0) {
      throw new ForbiddenException(
        'This conversation is unavailable because one of the accounts is blocked',
      );
    }

    const existing = await sql<ConversationRow>`
      SELECT c.*,
             u1.display_name as "participant1Name",
             u2.display_name as "participant2Name",
             pr.pet_name as "reportpet_name",
             pr.status as "reportStatus",
             pr.photo_uri as "reportPhoto",
             (
               SELECT COUNT(*)::int FROM messages m
               WHERE m.conversation_id = c.id AND m.sender_id != ${user_id} AND m.read_at IS NULL AND m.deleted_at IS NULL
             ) as "unreadCount"
      FROM conversations c
      JOIN users u1 ON c.participant1_id = u1.id AND u1.deleted_at IS NULL
      JOIN users u2 ON c.participant2_id = u2.id AND u2.deleted_at IS NULL
      LEFT JOIN pet_reports pr ON pr.id = c.report_id AND pr.deleted_at IS NULL
      WHERE ((c.participant1_id = ${user_id} AND c.participant2_id = ${otherUserId}) OR (c.participant1_id = ${otherUserId} AND c.participant2_id = ${user_id}))
        AND (c.report_id = ${reportId} OR (${reportId}::uuid IS NULL AND c.report_id IS NULL))
        AND c.deleted_at IS NULL
      ORDER BY c.created_at DESC
      LIMIT 1
    `.execute(this.db);

    if (existing.rows.length > 0) {
      return { status: true, statusCode: 200, data: normalizeConversationRow(existing.rows[0], user_id) };
    }

    const normalized = normalizeConversationInput(user_id, otherUserId);
    const created = await sql<{ id: string }>`
      INSERT INTO conversations (participant1_id, participant2_id, report_id)
      VALUES (${normalized.participant1Id}, ${normalized.participant2Id}, ${reportId})
      RETURNING *
    `.execute(this.db);

    const hydrated = await this.findConversationByIdForUser(created.rows[0].id, user_id);

    return {
      status: true,
      statusCode: 201,
      data: normalizeConversationRow(hydrated as ConversationRow, user_id),
    };
  }

  async getMessages(user_id: string, conversationId: string, limit = 50) {
    const conversation = await this.findConversationByIdForUser(conversationId, user_id);
    if (!conversation) {
      throw new ForbiddenException('Conversation not found');
    }

    const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 100)) : 50;

    const result = await sql<MessageRow>`
      SELECT m.id, m.conversation_id as "conversationId", m.sender_id as "senderId",
             u.display_name as "senderName", m.text, m.read_at as "readAt", m.created_at
      FROM messages m
      JOIN users u ON u.id = m.sender_id AND u.deleted_at IS NULL
      WHERE m.conversation_id = ${conversationId} AND m.deleted_at IS NULL
      ORDER BY m.created_at DESC
      LIMIT ${safeLimit}
    `.execute(this.db);

    const rows = result.rows.reverse();
    const hydrated = await this.hydrateMessages(rows, user_id);

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

  async sendMessage(senderId: string, conversationId: string, text: string) {
    const valid = this.validateMessageText(text);
    if (!valid.ok || !valid.value) {
      throw new BadRequestException(valid.message || 'Invalid message');
    }

    const conversation = await this.findConversationByIdForUser(conversationId, senderId);
    if (!conversation) {
      throw new ForbiddenException('Conversation not found');
    }

    const result = await sql<MessageRow>`
      INSERT INTO messages (conversation_id, sender_id, text)
      VALUES (${conversationId}, ${senderId}, ${valid.value})
      RETURNING id, conversation_id as "conversationId", sender_id as "senderId", text, read_at as "readAt", created_at
    `.execute(this.db);

    const messageRow = result.rows[0];

    await sql`
      UPDATE conversations SET last_message_text = ${valid.value}, last_message_at = ${messageRow.created_at}
      WHERE id = ${conversationId} AND deleted_at IS NULL
    `.execute(this.db);

    const senderNameResult = await sql<{ display_name: string }>`
      SELECT display_name FROM users WHERE id = ${senderId} AND deleted_at IS NULL LIMIT 1
    `.execute(this.db);

    const hydratedRow: MessageRow = {
      ...messageRow,
      senderName: senderNameResult.rows[0]?.display_name || '',
    };

    const hydratedMessage = await this.hydrateMessages([hydratedRow], senderId);

    return { status: true, statusCode: 201, data: hydratedMessage[0] };
  }

  async markAsRead(user_id: string, conversationId: string) {
    const conversation = await this.findConversationByIdForUser(conversationId, user_id);
    if (!conversation) {
      throw new ForbiddenException('Conversation not found');
    }

    const result = await sql<{ id: string }>`
      UPDATE messages SET read_at = NOW()
      WHERE conversation_id = ${conversationId} AND sender_id != ${user_id} AND read_at IS NULL AND deleted_at IS NULL
      RETURNING id
    `.execute(this.db);

    return {
      status: true,
      statusCode: 200,
      data: { message: 'Messages marked as read', updatedCount: result.rows.length },
    };
  }

  async getConversationSummary(user_id: string, conversationId: string) {
    const conversation = await this.findConversationByIdForUser(conversationId, user_id);
    if (!conversation) {
      throw new ForbiddenException('Conversation not found');
    }

    const unreadResult = await sql<{ count: number }>`
      SELECT COUNT(*)::int as count FROM messages
      WHERE conversation_id = ${conversationId} AND sender_id != ${user_id} AND read_at IS NULL AND deleted_at IS NULL
    `.execute(this.db);

    const row = { ...conversation, unreadCount: unreadResult.rows[0]?.count || 0 } as ConversationRow;

    return { status: true, statusCode: 200, data: normalizeConversationRow(row, user_id) };
  }
}
