import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

import { Lead, CLIENT_GROUP_LABELS } from '../entities/lead.entity';
import { User } from '../../user/entities/user.entity';

// ─────────────────────────────────────────────────────────────────────────
// ⚠ ADAPTER SECTION — the ONLY part of this file tied to your existing
// notifications/mails modules. Everything below it is plain logic.
//
// I could not see audit-requests.service.ts (only the controller and module
// were in the zip), so these two interfaces describe the shape this service
// expects. Compare them against the real NotificationsService / MailsService
// you call from audit-requests, and if a method name or argument differs,
// change it HERE — nothing else in the module touches those services.
//
// Real-time delivery: this deliberately does NOT open its own socket. Your
// audit-request notifications already arrive live, which means whatever
// NotificationsService.create() does internally (gateway emit, SSE push,
// Redis publish) is the live channel. Calling the same method puts lead
// events on that same pipe, so the frontend needs no new transport.
// ─────────────────────────────────────────────────────────────────────────

export interface NotificationsPort {
  create(payload: {
    user_id: number;
    type: string;
    title: string;
    message: string;
    link?: string | null;
    metadata?: Record<string, any> | null;
  }): Promise<any>;
}

export interface MailsPort {
  sendMail(payload: {
    to: string;
    subject: string;
    html: string;
  }): Promise<any>;
}

/** Optional: a leads-specific socket room, if you want one beyond the shared feed. */
export interface LeadsGatewayPort {
  emitToUser(userId: number, event: string, payload: any): void;
}

export const NOTIFICATIONS_PORT = 'NOTIFICATIONS_PORT';
export const MAILS_PORT = 'MAILS_PORT';
export const LEADS_GATEWAY_PORT = 'LEADS_GATEWAY_PORT';

// Add these three to your NotificationType enum (same file where you added
// AUDIT_REQUEST_SUBMITTED etc.):
export const LEAD_NOTIFICATION_TYPES = {
  ASSIGNED: 'LEAD_ASSIGNED',
  UNASSIGNED: 'LEAD_UNASSIGNED',
  BULK_ASSIGNED: 'LEAD_BULK_ASSIGNED',
  HANDOVER_REQUESTED: 'LEAD_HANDOVER_REQUESTED',
} as const;

@Injectable()
export class LeadNotificationsService {
  private readonly logger = new Logger('LeadNotificationsService');

  constructor(
    @InjectDataSource('scheme_dbs')
    private readonly dataSource: DataSource,

    @Inject(NOTIFICATIONS_PORT)
    private readonly notifications: NotificationsPort,

    @Inject(MAILS_PORT)
    private readonly mails: MailsPort,

    @Optional()
    @Inject(LEADS_GATEWAY_PORT)
    private readonly gateway?: LeadsGatewayPort,
  ) {}

  // ═══════════════════════════════════════════════════════════════════════
  // PUBLIC API — called from LeadService, always fire-and-forget.
  // Every method swallows its own errors: a mail server hiccup must never
  // roll back a reassignment that already committed.
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * A lead moved from one person to another.
   *
   * Three people can care, and they get different messages:
   *   • the NEW owner   — "you now own this", actionable, gets an email
   *   • the OLD owner   — "this left your queue", in-app only, no email
   *   • the actor       — nothing (they just did it, they know)
   */
  async leadAssigned(params: {
    leadId: number;
    newAssigneeId: number;
    previousAssigneeId: number | null;
    actorId: number;
    note?: string | null;
  }): Promise<void> {
    try {
      const { leadId, newAssigneeId, previousAssigneeId, actorId, note } =
        params;

      if (newAssigneeId === previousAssigneeId) return;

      const lead = await this.dataSource
        .getRepository(Lead)
        .findOne({ where: { id: leadId } });
      if (!lead) return;

      const [newOwner, oldOwner, actor] = await Promise.all([
        this.findUser(newAssigneeId),
        previousAssigneeId ? this.findUser(previousAssigneeId) : null,
        this.findUser(actorId),
      ]);

      const actorName = this.displayName(actor) ?? 'A colleague';
      const link = `/modules/leads/${lead.id}`;
      const summary = this.leadSummary(lead);

      // ── 1. The new owner ─────────────────────────────────────────────
      if (newOwner && newAssigneeId !== actorId) {
        await this.push({
          user_id: newAssigneeId,
          type: LEAD_NOTIFICATION_TYPES.ASSIGNED,
          title: `Lead ${lead.lead_code} is yours`,
          message: `${actorName} assigned ${lead.company} to you.${
            note ? ` Note: ${note}` : ''
          }`,
          link,
          metadata: {
            lead_id: lead.id,
            lead_code: lead.lead_code,
            company: lead.company,
            client_group: lead.client_group,
            previous_owner_id: previousAssigneeId,
            assigned_by: actorId,
          },
        });

        const email = this.emailOf(newOwner);
        if (email) {
          await this.mail({
            to: email,
            subject: `Lead assigned to you — ${lead.lead_code} · ${lead.company}`,
            html: this.assignedEmailHtml({
              recipientName: this.displayName(newOwner) ?? 'there',
              actorName,
              lead,
              summary,
              note: note ?? null,
              link,
            }),
          });
        }
      }

      // ── 2. The person who had it ─────────────────────────────────────
      // In-app only. Losing a lead is information, not an action item, and
      // an email for it is the kind of thing people build filters for.
      if (oldOwner && previousAssigneeId && previousAssigneeId !== actorId) {
        await this.push({
          user_id: previousAssigneeId,
          type: LEAD_NOTIFICATION_TYPES.UNASSIGNED,
          title: `Lead ${lead.lead_code} moved to ${this.displayName(newOwner) ?? 'another owner'}`,
          message: `${actorName} reassigned ${lead.company}. It is no longer in your queue.`,
          link,
          metadata: {
            lead_id: lead.id,
            lead_code: lead.lead_code,
            new_owner_id: newAssigneeId,
          },
        });
      }

      this.logger.log(
        `Lead ${lead.lead_code}: notified ${newAssigneeId}` +
          (previousAssigneeId ? ` and ${previousAssigneeId}` : ''),
      );
    } catch (err: any) {
      this.logger.error(`leadAssigned notification failed: ${err.message}`);
    }
  }

  /** One summary notification per batch, not one per lead. */
  async leadsBulkAssigned(params: {
    leadIds: number[];
    newAssigneeId: number;
    actorId: number;
    note?: string | null;
  }): Promise<void> {
    try {
      const { leadIds, newAssigneeId, actorId, note } = params;
      if (!leadIds.length || newAssigneeId === actorId) return;

      const [newOwner, actor] = await Promise.all([
        this.findUser(newAssigneeId),
        this.findUser(actorId),
      ]);
      if (!newOwner) return;

      const actorName = this.displayName(actor) ?? 'A colleague';
      const leads = await this.dataSource
        .getRepository(Lead)
        .createQueryBuilder('l')
        .where('l.id IN (:...ids)', { ids: leadIds })
        .getMany();

      await this.push({
        user_id: newAssigneeId,
        type: LEAD_NOTIFICATION_TYPES.BULK_ASSIGNED,
        title: `${leads.length} lead${leads.length === 1 ? '' : 's'} assigned to you`,
        message: `${actorName} moved ${leads.length} lead${
          leads.length === 1 ? '' : 's'
        } into your queue.${note ? ` Note: ${note}` : ''}`,
        link: `/modules/leads?assigned_to=${newAssigneeId}`,
        metadata: { lead_ids: leadIds, assigned_by: actorId },
      });

      const email = this.emailOf(newOwner);
      if (email) {
        await this.mail({
          to: email,
          subject: `${leads.length} lead${leads.length === 1 ? '' : 's'} assigned to you`,
          html: this.bulkEmailHtml({
            recipientName: this.displayName(newOwner) ?? 'there',
            actorName,
            leads,
            note: note ?? null,
          }),
        });
      }
    } catch (err: any) {
      this.logger.error(`leadsBulkAssigned notification failed: ${err.message}`);
    }
  }

  /** Someone wants a lead they don't own — ping the owner + anyone who can assign. */
  async handoverRequested(params: {
    lead: Lead;
    requesterId: number;
    requesterName: string;
    reason: string;
    note?: string | null;
  }): Promise<void> {
    try {
      const { lead, requesterId, requesterName, reason, note } = params;

      const recipients = new Set<number>();
      if (lead.assigned_to && lead.assigned_to !== requesterId) {
        recipients.add(lead.assigned_to);
      }
      for (const id of await this.usersWhoCanAssign()) {
        if (id !== requesterId) recipients.add(id);
      }

      for (const userId of recipients) {
        await this.push({
          user_id: userId,
          type: LEAD_NOTIFICATION_TYPES.HANDOVER_REQUESTED,
          title: `${requesterName} wants lead ${lead.lead_code}`,
          message: `${lead.company} — reason: ${reason}${note ? ` · ${note}` : ''}`,
          link: `/modules/leads/${lead.id}`,
          metadata: {
            lead_id: lead.id,
            requester_id: requesterId,
            reason,
          },
        });
      }
    } catch (err: any) {
      this.logger.error(`handoverRequested notification failed: ${err.message}`);
    }
  }

  /** New lead landed — tell the assignee if it isn't the person who created it. */
  async leadCreated(leadId: number, actorId: number): Promise<void> {
    try {
      const lead = await this.dataSource
        .getRepository(Lead)
        .findOne({ where: { id: leadId } });
      if (!lead?.assigned_to || lead.assigned_to === actorId) return;

      await this.leadAssigned({
        leadId,
        newAssigneeId: lead.assigned_to,
        previousAssigneeId: null,
        actorId,
        note: null,
      });
    } catch (err: any) {
      this.logger.error(`leadCreated notification failed: ${err.message}`);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // ADAPTER — two methods. Point these at your real services.
  // ═══════════════════════════════════════════════════════════════════════

  private async push(payload: {
    user_id: number;
    type: string;
    title: string;
    message: string;
    link?: string | null;
    metadata?: Record<string, any> | null;
  }) {
    // 👉 If your NotificationsService method is named send() / notify() /
    //    createNotification(), or takes camelCase keys, remap here.
    await this.notifications.create(payload);

    // Optional second channel, only if you registered LEADS_GATEWAY_PORT.
    this.gateway?.emitToUser(payload.user_id, 'lead:notification', payload);
  }

  private async mail(payload: { to: string; subject: string; html: string }) {
    // 👉 If your MailsService method is sendEmail() / send(), remap here.
    await this.mails.sendMail(payload);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // HELPERS
  // ═══════════════════════════════════════════════════════════════════════

  private async findUser(id: number | null): Promise<User | null> {
    if (!id) return null;
    return this.dataSource.getRepository(User).findOne({ where: { id } as any });
  }

  /** Your User entity uses firstName/lastName rather than a single `name`. */
  private displayName(user: User | null | undefined): string | null {
    if (!user) return null;
    const first = (user as any).firstName ?? '';
    const last = (user as any).lastName ?? '';
    const full = `${first} ${last}`.trim();
    return full || (user as any).email || null;
  }

  private emailOf(user: User | null | undefined): string | null {
    const email = (user as any)?.email;
    return typeof email === 'string' && email.includes('@') ? email : null;
  }

  private leadSummary(lead: Lead): string {
    const bits = [
      lead.company,
      lead.contact,
      lead.client_group ? CLIENT_GROUP_LABELS[lead.client_group] : null,
      lead.status,
    ].filter(Boolean);
    return bits.join(' · ');
  }

  /**
   * Users holding the 'assign' permission on the leads module — same join
   * shape LeadService.userHasPermission() uses.
   */
  private async usersWhoCanAssign(): Promise<number[]> {
    try {
      const rows = await this.dataSource.query(
        `
        SELECT DISTINCT u.id AS id
        FROM users u
        INNER JOIN user_roles ur ON ur.user_id = u.id
        INNER JOIN role_permissions rp ON rp.role_id = ur.role_id
        INNER JOIN permissions p ON p.id = rp.permission_id
        INNER JOIN modules m ON m.id = p.module_id
        WHERE p.action = 'assign' AND m.slug = 'leads'
        `,
      );
      return rows.map((r: any) => Number(r.id));
    } catch {
      return [];
    }
  }

  // ── Email templates ────────────────────────────────────────────────────
  // Inline styles only: Outlook and Gmail both strip <style> blocks.

  private shell(bodyHtml: string): string {
    return `
<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f8fafc;padding:24px;">
  <div style="max-width:560px;margin:0 auto;background:#ffffff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;">
    <div style="background:linear-gradient(135deg,#0f766e 0%,#14b8a6 100%);padding:18px 24px;">
      <span style="color:#ffffff;font-size:15px;font-weight:700;letter-spacing:0.02em;">Lead Management</span>
    </div>
    <div style="padding:24px;color:#1f2937;font-size:14px;line-height:1.6;">
      ${bodyHtml}
    </div>
    <div style="padding:14px 24px;background:#f8fafc;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:11px;">
      Sent automatically when a lead changes hands. Reply to this address and a human will see it.
    </div>
  </div>
</div>`.trim();
  }

  private button(href: string, label: string): string {
    const base = process.env.APP_URL ?? '';
    return `
<a href="${base}${href}"
   style="display:inline-block;margin:18px 0 4px;padding:11px 22px;background:#0f766e;color:#ffffff;
          text-decoration:none;border-radius:8px;font-size:14px;font-weight:600;">
  ${label}
</a>`.trim();
  }

  private assignedEmailHtml(p: {
    recipientName: string;
    actorName: string;
    lead: Lead;
    summary: string;
    note: string | null;
    link: string;
  }): string {
    return this.shell(`
      <p style="margin:0 0 14px;">Hi ${this.escape(p.recipientName)},</p>
      <p style="margin:0 0 16px;">
        ${this.escape(p.actorName)} assigned a lead to you.
      </p>
      <table style="width:100%;border-collapse:collapse;margin:0 0 8px;">
        ${this.row('Lead', p.lead.lead_code)}
        ${this.row('Company', p.lead.company)}
        ${p.lead.contact ? this.row('Contact', p.lead.contact) : ''}
        ${p.lead.phone ? this.row('Phone', p.lead.phone) : ''}
        ${p.lead.email ? this.row('Email', p.lead.email) : ''}
        ${
          p.lead.client_group
            ? this.row('Client group', CLIENT_GROUP_LABELS[p.lead.client_group])
            : ''
        }
        ${this.row('Status', p.lead.status)}
        ${this.row('Priority', p.lead.priority)}
      </table>
      ${
        p.note
          ? `<div style="margin:14px 0;padding:12px 14px;background:#f0fdfa;border-left:3px solid #14b8a6;border-radius:0 6px 6px 0;">
               <div style="font-size:11px;font-weight:700;color:#0f766e;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px;">Note from ${this.escape(p.actorName)}</div>
               <div>${this.escape(p.note)}</div>
             </div>`
          : ''
      }
      ${this.button(p.link, 'Open the lead')}
    `);
  }

  private bulkEmailHtml(p: {
    recipientName: string;
    actorName: string;
    leads: Lead[];
    note: string | null;
  }): string {
    const shown = p.leads.slice(0, 15);
    const rest = p.leads.length - shown.length;

    return this.shell(`
      <p style="margin:0 0 14px;">Hi ${this.escape(p.recipientName)},</p>
      <p style="margin:0 0 16px;">
        ${this.escape(p.actorName)} moved ${p.leads.length} lead${p.leads.length === 1 ? '' : 's'} into your queue.
      </p>
      <table style="width:100%;border-collapse:collapse;">
        ${shown
          .map((l) => this.row(l.lead_code, l.company))
          .join('')}
      </table>
      ${rest > 0 ? `<p style="margin:10px 0 0;color:#64748b;font-size:13px;">…and ${rest} more.</p>` : ''}
      ${
        p.note
          ? `<div style="margin:14px 0;padding:12px 14px;background:#f0fdfa;border-left:3px solid #14b8a6;border-radius:0 6px 6px 0;">${this.escape(p.note)}</div>`
          : ''
      }
      ${this.button('/modules/leads', 'Open my leads')}
    `);
  }

  private row(label: string, value: string | null): string {
    if (!value) return '';
    return `
      <tr>
        <td style="padding:6px 12px 6px 0;color:#64748b;font-size:12px;white-space:nowrap;vertical-align:top;">${this.escape(label)}</td>
        <td style="padding:6px 0;color:#111827;font-size:13px;font-weight:600;">${this.escape(value)}</td>
      </tr>`;
  }

  private escape(v: string): string {
    return String(v)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }
}
