import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Contact } from '@prisma/client';
import { AuthenticatedUser } from '../auth/auth.types';
import { normalizeText } from '../common/normalize';
import { PrismaService } from '../prisma/prisma.service';

type ConversationFilter = 'all' | 'active' | 'pending';

@Injectable()
export class ConversationsService {
  constructor(private readonly prisma: PrismaService) {}

  async findAll(query: { filter?: string; search?: string }) {
    const contacts = await this.prisma.contact.findMany({
      orderBy: { createdAt: 'desc' },
    });

    const items = await Promise.all(contacts.map((contact) => this.buildConversationSummary(contact)));
    const normalizedFilter = this.normalizeFilter(query.filter);
    const searchNeedle = query.search?.trim().toLowerCase();

    return items
      .filter((item) => {
        const matchesFilter =
          normalizedFilter === 'all'
            ? true
            : normalizedFilter === 'pending'
              ? item.status === 'PENDING'
              : item.status === 'ACTIVE' || item.status === 'NEW';

        const matchesSearch =
          !searchNeedle ||
          [item.name, item.email, item.phone, item.topic, item.snippet, item.location]
            .filter(Boolean)
            .some((value) => String(value).toLowerCase().includes(searchNeedle));

        return matchesFilter && matchesSearch;
      })
      .sort((left, right) => new Date(right.lastActivityAt).getTime() - new Date(left.lastActivityAt).getTime());
  }

  async findOne(contactId: string) {
    const contact = await this.prisma.contact.findUnique({
      where: { id: contactId },
    });

    if (!contact) {
      throw new NotFoundException('Conversation not found');
    }

    await this.prisma.conversationMessage.updateMany({
      where: {
        contactId,
        direction: 'incoming',
        readAt: null,
      },
      data: {
        readAt: new Date(),
      },
    });

    return this.buildConversationDetail(contact);
  }

  async sendMessage(contactId: string, dto: { body: string }, user: AuthenticatedUser, ipAddress?: string) {
    const contact = await this.prisma.contact.findUnique({
      where: { id: contactId },
    });

    if (!contact) {
      throw new NotFoundException('Conversation not found');
    }

    const actor = await this.prisma.user.findUnique({
      where: { id: user.sub },
      select: { id: true, name: true, email: true },
    });

    const body = normalizeText(dto.body);
    if (!body) {
      throw new BadRequestException('Message body is required');
    }

    const providerSend = await this.sendViaConfiguredProvider(contact.phoneNumber, body);
    const message = await this.prisma.conversationMessage.create({
      data: {
        contactId,
        direction: 'outgoing',
        source: providerSend.provider,
        status: providerSend.status,
        body,
        senderUserId: actor?.id || user.sub,
        senderName: actor?.name || user.email,
        externalMessageId: providerSend.externalMessageId,
      },
    });

    await this.prisma.contact.update({
      where: { id: contactId },
      data: {
        assignedUserId: actor?.id || user.sub,
        assignedUserName: actor?.name || user.email,
        assignedAt: new Date(),
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor?.id || user.sub,
        actorName: actor?.name || user.email,
        actorEmail: actor?.email || user.email,
        actionType: 'Conversation Reply Sent',
        module: 'Conversations',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Sent reply to ${contact.name} (${contact.phoneNumber}) via ${providerSend.provider}.`,
      },
    });

    return {
      success: true,
      message: this.mapThreadMessage(message),
    };
  }

  async assignToCurrentUser(contactId: string, user: AuthenticatedUser, ipAddress?: string) {
    const actor = await this.prisma.user.findUnique({
      where: { id: user.sub },
      select: { id: true, name: true, email: true },
    });

    const contact = await this.prisma.contact.findUnique({
      where: { id: contactId },
    });

    if (!contact) {
      throw new NotFoundException('Conversation not found');
    }

    await this.prisma.contact.update({
      where: { id: contactId },
      data: {
        assignedUserId: actor?.id || user.sub,
        assignedUserName: actor?.name || user.email,
        assignedAt: new Date(),
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor?.id || user.sub,
        actorName: actor?.name || user.email,
        actorEmail: actor?.email || user.email,
        actionType: 'Conversation Assigned',
        module: 'Conversations',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Assigned conversation ${contact.name} (${contact.phoneNumber}) to ${actor?.name || user.email}.`,
      },
    });

    return { success: true };
  }

  async syncInboundFromWebhookEvent(input: {
    webhookEventId: string;
    contactId: string;
    externalMessageId?: string | null;
    body: string;
    occurredAt: Date;
  }) {
    const normalizedBody = normalizeText(input.body) || 'Inbound message received.';
    await this.prisma.conversationMessage.upsert({
      where: { webhookEventId: input.webhookEventId },
      update: {
        body: normalizedBody,
        externalMessageId: input.externalMessageId || undefined,
        occurredAt: input.occurredAt,
        status: 'received',
      },
      create: {
        contactId: input.contactId,
        direction: 'incoming',
        messageType: 'text',
        source: 'webhook',
        status: 'received',
        body: normalizedBody,
        externalMessageId: input.externalMessageId || undefined,
        webhookEventId: input.webhookEventId,
        occurredAt: input.occurredAt,
      },
    });
  }

  private async buildConversationSummary(contact: Contact) {
    const [messages, webhooks] = await Promise.all([
      this.prisma.conversationMessage.findMany({
        where: { contactId: contact.id },
        orderBy: { occurredAt: 'desc' },
        take: 20,
      }),
      this.prisma.webhookEvent.findMany({
        where: {
          OR: [{ senderPhone: contact.phoneNumber }, { recipientPhone: contact.phoneNumber }],
        },
        orderBy: { createdAt: 'desc' },
        take: 10,
      }),
    ]);

    const latestMessage = messages[0] || null;
    const latestWebhook = webhooks[0] || null;
    const latestMessageAt = latestMessage?.occurredAt?.getTime() || 0;
    const latestWebhookAt = latestWebhook?.createdAt?.getTime() || 0;
    const lastActivityAt = new Date(Math.max(latestMessageAt, latestWebhookAt, contact.updatedAt.getTime()));
    const latestSnippet =
      latestMessageAt >= latestWebhookAt
        ? latestMessage?.body
        : this.extractWebhookSnippet(latestWebhook?.payloadJson) || latestWebhook?.eventType || 'No activity yet';
    const tags = this.deriveTags(contact);
    const unreadCount = messages.filter((message) => message.direction === 'incoming' && !message.readAt).length;
    const fallbackInboundUnread =
      unreadCount === 0 &&
      latestWebhook?.eventType === 'message.inbound' &&
      latestWebhookAt > latestMessageAt;
    const status = fallbackInboundUnread || unreadCount > 0
      ? 'NEW'
      : lastActivityAt.getTime() >= Date.now() - 1000 * 60 * 60 * 24
        ? 'ACTIVE'
        : 'PENDING';

    return {
      id: contact.id,
      name: contact.name,
      initials: this.getInitials(contact.name),
      time: this.relativeTime(lastActivityAt),
      status,
      tone: status === 'PENDING' ? 'warning' : status === 'ACTIVE' ? 'success' : 'info',
      topic: tags[0]?.toUpperCase() || 'GENERAL',
      snippet: latestSnippet || 'No activity yet',
      online: lastActivityAt.getTime() >= Date.now() - 1000 * 60 * 10,
      location: this.deriveLocation(contact.phoneNumber, contact.email),
      email: contact.email || 'N/A',
      phone: contact.phoneNumber,
      customerSince: `Customer since ${contact.createdAt.toLocaleDateString('en-US', {
        month: 'short',
        year: 'numeric',
        timeZone: 'UTC',
      })}`,
      tags,
      lastActivityAt: lastActivityAt.toISOString(),
      unreadCount,
      assignedAgentName: contact.assignedUserName || null,
    };
  }

  private async buildConversationDetail(contact: Contact) {
    const [messages, webhookEvents, auditLogs] = await Promise.all([
      this.prisma.conversationMessage.findMany({
        where: { contactId: contact.id },
        orderBy: { occurredAt: 'asc' },
        take: 200,
      }),
      this.prisma.webhookEvent.findMany({
        where: {
          OR: [{ senderPhone: contact.phoneNumber }, { recipientPhone: contact.phoneNumber }],
        },
        orderBy: { createdAt: 'desc' },
        take: 10,
      }),
      this.prisma.auditLog.findMany({
        where: {
          OR: [
            { details: { contains: contact.phoneNumber, mode: 'insensitive' } },
            { details: { contains: contact.name, mode: 'insensitive' } },
          ],
        },
        orderBy: { createdAt: 'desc' },
        take: 6,
      }),
    ]);

    const mirroredWebhookIds = new Set(messages.map((message) => message.webhookEventId).filter(Boolean));
    const fallbackInboundMessages = webhookEvents
      .filter((event) => event.eventType === 'message.inbound' && !mirroredWebhookIds.has(event.eventId))
      .map((event) => ({
        id: `webhook-${event.eventId}`,
        direction: 'incoming' as const,
        body: this.extractWebhookSnippet(event.payloadJson) || 'Inbound message received.',
        time: this.formatThreadTime(event.createdAt),
        occurredAt: event.createdAt.toISOString(),
      }));

    const threadMessages = [
      ...messages.map((message) => this.mapThreadMessage(message)),
      ...fallbackInboundMessages,
    ].sort((left, right) => new Date(left.occurredAt).getTime() - new Date(right.occurredAt).getTime());

    const summary = await this.buildConversationSummary(contact);

    return {
      ...summary,
      messages: threadMessages.map(({ occurredAt: _occurredAt, ...message }) => message),
      activity: this.buildRecentActivity(webhookEvents, auditLogs),
      assignedAgentName: contact.assignedUserName || summary.assignedAgentName,
    };
  }

  private mapThreadMessage(message: {
    id: string;
    direction: string;
    body: string;
    occurredAt: Date;
    status?: string;
  }) {
    return {
      id: message.id,
      direction: message.direction === 'outgoing' ? 'outgoing' : 'incoming',
      body: message.body,
      time: this.formatThreadTime(message.occurredAt),
      status: message.status || 'sent',
      occurredAt: message.occurredAt.toISOString(),
    };
  }

  private buildRecentActivity(
    webhookEvents: Array<{ id: string; eventType: string; createdAt: Date; processingStatus: string }>,
    auditLogs: Array<{ id: string; actionType: string; createdAt: Date; details: string; severity: string }>,
  ) {
    return [
      ...webhookEvents.map((event) => ({
        id: `webhook-${event.id}`,
        title: event.eventType,
        meta: `${event.processingStatus} • ${this.relativeTime(event.createdAt)}`,
        tone: event.eventType === 'message.inbound' ? 'primary' : 'muted',
        createdAt: event.createdAt.toISOString(),
      })),
      ...auditLogs.map((log) => ({
        id: `audit-${log.id}`,
        title: log.actionType,
        meta: `${log.severity} • ${this.relativeTime(log.createdAt)}`,
        tone: log.severity === 'alert' ? 'primary' : 'muted',
        createdAt: log.createdAt.toISOString(),
      })),
    ]
      .sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime())
      .slice(0, 5)
      .map(({ createdAt: _createdAt, ...item }) => item);
  }

  private normalizeFilter(value?: string): ConversationFilter {
    const normalized = value?.trim().toLowerCase();
    if (normalized === 'active' || normalized === 'pending') {
      return normalized;
    }

    return 'all';
  }

  private extractWebhookSnippet(payload: unknown) {
    if (!payload || typeof payload !== 'object') {
      return '';
    }

    const record = payload as Record<string, unknown>;
    const textRecord =
      record.text && typeof record.text === 'object' && !Array.isArray(record.text)
        ? (record.text as Record<string, unknown>)
        : null;
    const interactiveRecord =
      record.interactive && typeof record.interactive === 'object' && !Array.isArray(record.interactive)
        ? (record.interactive as Record<string, unknown>)
        : null;

    return [
      typeof textRecord?.body === 'string' ? textRecord.body : null,
      typeof record.body === 'string' ? record.body : null,
      typeof record.caption === 'string' ? record.caption : null,
      typeof interactiveRecord?.title === 'string' ? interactiveRecord.title : null,
      typeof record.type === 'string' ? `[${record.type}]` : null,
    ].find((value) => typeof value === 'string' && value.trim()) || '';
  }

  private deriveTags(contact: { company: string | null; notes: string | null; createdAt: Date; email: string | null }) {
    const haystack = `${contact.company || ''} ${contact.notes || ''} ${contact.email || ''}`.toLowerCase();
    const tags = new Set<string>();
    if (haystack.includes('vip') || haystack.includes('premium')) tags.add('Premium');
    if (haystack.includes('support') || haystack.includes('help') || haystack.includes('issue')) tags.add('Support');
    if (haystack.includes('lead') || haystack.includes('prospect')) tags.add('Lead');
    if (haystack.includes('retail') || haystack.includes('shop') || haystack.includes('store')) tags.add('Inquiry');
    if (contact.createdAt.getTime() >= Date.now() - 1000 * 60 * 60 * 24 * 30) tags.add('New');
    if (tags.size === 0) tags.add('General');
    return Array.from(tags);
  }

  private deriveLocation(phoneNumber: string, email?: string | null) {
    const value = phoneNumber.replace(/\s+/g, '');
    if (value.startsWith('+62') || value.startsWith('62')) return 'Jakarta, ID';
    if (value.startsWith('+65') || value.startsWith('65')) return 'Singapore, SG';
    if (value.startsWith('+1') || value.startsWith('1')) return 'San Francisco, CA';
    if (email?.endsWith('.es')) return 'Madrid, ES';
    if (email?.endsWith('.net')) return 'London, UK';
    return 'Regional Contact';
  }

  private relativeTime(date: Date) {
    const diffMs = Date.now() - date.getTime();
    const diffMinutes = Math.max(1, Math.floor(diffMs / (1000 * 60)));
    if (diffMinutes < 60) return `${diffMinutes}m ago`;
    const diffHours = Math.floor(diffMinutes / 60);
    if (diffHours < 24) return `${diffHours}h ago`;
    const diffDays = Math.floor(diffHours / 24);
    if (diffDays < 7) return `${diffDays}d ago`;
    return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  }

  private formatThreadTime(date: Date) {
    return new Intl.DateTimeFormat('en-US', {
      hour: 'numeric',
      minute: '2-digit',
      hour12: true,
    }).format(date);
  }

  private getInitials(name: string) {
    return name
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 2)
      .map((part) => part[0]?.toUpperCase() || '')
      .join('');
  }

  private async sendViaConfiguredProvider(phoneNumber: string, body: string) {
    const stored = await this.prisma.integrationConfig.findUnique({
      where: { configKey: 'whatsapp' },
    });
    const configJson = (stored?.configJson as Record<string, unknown> | null) ?? {};
    const provider = String(configJson.provider || stored?.provider || 'internal').toLowerCase();
    const accessToken = typeof configJson.accessToken === 'string' ? configJson.accessToken : '';
    const phoneNumberId = typeof configJson.phoneNumberId === 'string' ? configJson.phoneNumberId : '';

    if (stored?.isEnabled && provider === 'meta' && accessToken && phoneNumberId) {
      const response = await fetch(`https://graph.facebook.com/v20.0/${phoneNumberId}/messages`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify({
          messaging_product: 'whatsapp',
          recipient_type: 'individual',
          to: phoneNumber.replace(/\D+/g, ''),
          type: 'text',
          text: {
            preview_url: false,
            body,
          },
        }),
      });

      const payload = (await response.json().catch(() => ({}))) as {
        messages?: Array<{ id?: string }>;
      };

      if (!response.ok) {
        return {
          provider: 'meta',
          status: 'failed',
          externalMessageId: null,
        };
      }

      return {
        provider: 'meta',
        status: 'queued',
        externalMessageId: payload.messages?.[0]?.id || null,
      };
    }

    return {
      provider: 'internal',
      status: 'sent',
      externalMessageId: null,
    };
  }
}
