import type { Db } from '../types/db';

const nodemailer = require('nodemailer');
const { getDb, getSetting } = require('../db/database.cts');
const { decryptSecret, encryptSecret } = require('./encryptionService.cts');
const { accountingActiveSql } = require('./paymentAccountingService.cts');
const {
  markNotificationError,
  markNotificationSuccess,
  markNotificationTestSuccess,
} = require('./statusRuntime.cts');
const { localDateString } = require('../utils/dates.mts') as typeof import('../utils/dates.mts');
const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');

type Row = Record<string, any>;

// ── Push notification channels ────────────────────────────────────────────────

const PUSH_PRIORITY: Record<string, string> = {
  upcoming: 'default',
  soon: 'high',
  today: 'high',
  overdue: 'urgent',
};
const PUSH_TAGS: Record<string, string[]> = {
  upcoming: ['bell'],
  soon: ['warning'],
  today: ['rotating_light'],
  overdue: ['rotating_light', 'red_circle'],
};
const DISCORD_COLOR: Record<string, number> = {
  upcoming: 0x4f46e5,
  soon: 0xd97706,
  today: 0xdc7308,
  overdue: 0xdc2626,
};

function safeDecrypt(stored: any): string {
  if (!stored) return '';
  try {
    return decryptSecret(stored);
  } catch {
    return stored;
  }
}

async function sendNtfy(url: string, token: any, title: string, body: string, urgency = 'soon') {
  const headers: Record<string, string> = {
    Title: title,
    Priority: PUSH_PRIORITY[urgency] || 'default',
    Tags: (PUSH_TAGS[urgency] || ['bell']).join(','),
    'Content-Type': 'text/plain',
  };
  if (token) headers['Authorization'] = `Bearer ${token}`;
  const res = await fetch(url, { method: 'POST', headers, body });
  if (!res.ok) throw new Error(`ntfy returned ${res.status}`);
}

async function sendGotify(url: string, token: any, title: string, body: string, urgency = 'soon') {
  const priority =
    ({ upcoming: 4, soon: 6, today: 7, overdue: 9 } as Record<string, number>)[urgency] ?? 5;
  const endpoint = url.replace(/\/$/, '') + '/message?token=' + encodeURIComponent(token);
  const res = await fetch(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, message: body, priority }),
  });
  if (!res.ok) throw new Error(`Gotify returned ${res.status}`);
}

async function sendDiscord(webhookUrl: string, title: string, body: string, urgency = 'soon') {
  const color = DISCORD_COLOR[urgency] ?? 0x4f46e5;
  const res = await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      embeds: [
        {
          title,
          description: body,
          color,
          footer: { text: 'Bill Tracker' },
          timestamp: new Date().toISOString(),
        },
      ],
    }),
  });
  if (!res.ok) throw new Error(`Discord returned ${res.status}`);
}

async function sendTelegram(botToken: any, chatId: any, title: string, body: string) {
  const text = `*${title}*\n${body}`;
  const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }),
  });
  if (!res.ok) {
    const err: any = await res.json().catch(() => ({}));
    throw new Error(`Telegram error: ${err.description || res.status}`);
  }
}

// Dispatch push to whichever channel this user has configured.
async function sendPushToUser(user: Row, title: string, body: string, urgency: any) {
  if (!user.notify_push_enabled || !user.push_channel || !user.push_url) return;
  const url = safeDecrypt(user.push_url);
  const token = safeDecrypt(user.push_token);
  switch (user.push_channel) {
    case 'ntfy':
      return sendNtfy(url, token, title, body, urgency);
    case 'gotify':
      return sendGotify(url, token, title, body, urgency);
    case 'discord':
      return sendDiscord(url, title, body, urgency);
    case 'telegram':
      return sendTelegram(token, user.push_chat_id, title, body);
    default:
      throw new Error(`Unknown push channel: ${user.push_channel}`);
  }
}

async function sendTestPush(user: Row) {
  await sendPushToUser(
    user,
    'Bill Tracker — Test Notification',
    'Your push notification channel is working correctly.',
    'upcoming',
  );
}

// ── SMTP transport ────────────────────────────────────────────────────────────

function getSmtpPassword(): string {
  const stored = getSetting('notify_smtp_password');
  if (!stored) return '';
  try {
    return decryptSecret(stored);
  } catch {
    return stored; // legacy plaintext — works until re-saved via admin UI
  }
}

function createTransport(): any {
  const host = getSetting('notify_smtp_host');
  const port = parseInt(getSetting('notify_smtp_port') || '587', 10);
  const encryption = getSetting('notify_smtp_encryption') || 'starttls';
  const username = getSetting('notify_smtp_username');
  const password = getSmtpPassword();
  const selfSigned = getSetting('notify_smtp_self_signed') === 'true';

  if (!host) throw new Error('SMTP host is not configured');

  return nodemailer.createTransport({
    host,
    port,
    secure: encryption === 'ssl',
    auth: username ? { user: username, pass: password } : undefined,
    tls: {
      rejectUnauthorized: !selfSigned,
      ...(encryption === 'none' ? { ignoreTLS: true } : {}),
    },
  });
}

function senderAddress(): string {
  const name = getSetting('notify_sender_name') || 'Bill Tracker';
  const address = getSetting('notify_sender_address');
  if (!address) throw new Error('Sender address is not configured');
  return `"${name}" <${address}>`;
}

// ── Email templates ───────────────────────────────────────────────────────────

function leadDaysOf(bill: any): number {
  return Number.isInteger(bill?.reminder_days_before) ? bill.reminder_days_before : 3;
}

function reminderTypeFor(bill: any, diffDays: number): string | null {
  const leadDays = leadDaysOf(bill);
  if (diffDays >= 2 && diffDays === leadDays) return 'due_3d';
  if (diffDays === 1) return 'due_1d';
  if (diffDays === 0) return 'due_today';
  if (diffDays < 0) return 'overdue';
  return null;
}

const TYPE_META: Record<string, { subject: (b: any) => string; urgency: string }> = {
  due_3d: {
    subject: (b) => `Reminder: ${b.name} due in ${leadDaysOf(b)} days`,
    urgency: 'upcoming',
  },
  due_1d: { subject: (b) => `Reminder: ${b.name} due tomorrow`, urgency: 'soon' },
  due_today: { subject: (b) => `Due today: ${b.name}`, urgency: 'today' },
  overdue: { subject: (b) => `Overdue: ${b.name} hasn't been paid`, urgency: 'overdue' },
};

const URGENCY_COLOR: Record<string, string> = {
  upcoming: '#4f46e5',
  soon: '#d97706',
  today: '#dc7308',
  overdue: '#dc2626',
};

function buildEmailHtml(bill: Row, type: string, dueDate: any): string {
  const meta = TYPE_META[type];
  const color = URGENCY_COLOR[meta.urgency];
  const amount = '$' + (fromCents(bill.expected_amount) || 0).toFixed(2);
  const fmt = (d: any) => {
    if (!d) return '—';
    const [y, m, day] = d.split('-');
    return `${parseInt(m)}/${parseInt(day)}/${y}`;
  };

  // QA-B14-04: escape the bill name everywhere it lands in the HTML.
  const name = esc(bill.name);
  const messages: Record<string, string> = {
    due_3d: `<strong>${name}</strong> is due in ${leadDaysOf(bill)} days.`,
    due_1d: `<strong>${name}</strong> is due <strong>tomorrow</strong>.`,
    due_today: `<strong>${name}</strong> is due <strong>today</strong>.`,
    overdue: `<strong>${name}</strong> was due on ${fmt(dueDate)} and has not been marked as paid.`,
  };

  return `<!DOCTYPE html>
<html>
<body style="margin:0;padding:0;background:#f5f6fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:32px 16px;">
  <tr><td align="center">
    <table width="540" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;border:1px solid #e2e4eb;overflow:hidden;">
      <tr>
        <td style="background:${color};padding:16px 24px;">
          <span style="color:white;font-size:18px;font-weight:700;">Bill Tracker</span>
        </td>
      </tr>
      <tr>
        <td style="padding:28px 24px;">
          <p style="margin:0 0 20px;font-size:15px;color:#1a1d2e;line-height:1.6;">
            ${messages[type]}
          </p>
          <table width="100%" cellpadding="0" cellspacing="0"
            style="border:1px solid #e2e4eb;border-radius:6px;overflow:hidden;font-size:14px;">
            <tr style="background:#f5f6fa;">
              <td style="padding:10px 14px;color:#6b7280;font-weight:600;border-bottom:1px solid #e2e4eb;">Bill</td>
              <td style="padding:10px 14px;border-bottom:1px solid #e2e4eb;">${esc(bill.name)}</td>
            </tr>
            <tr>
              <td style="padding:10px 14px;color:#6b7280;font-weight:600;border-bottom:1px solid #e2e4eb;">Due Date</td>
              <td style="padding:10px 14px;border-bottom:1px solid #e2e4eb;">${fmt(dueDate)}</td>
            </tr>
            <tr style="background:#f5f6fa;">
              <td style="padding:10px 14px;color:#6b7280;font-weight:600;">Amount</td>
              <td style="padding:10px 14px;font-weight:700;">${amount}</td>
            </tr>
          </table>
          <p style="margin:24px 0 0;font-size:12px;color:#9ca3af;">
            Sent by Bill Tracker &middot; ${new Date().toLocaleDateString()}
          </p>
        </td>
      </tr>
    </table>
  </td></tr>
</table>
</body>
</html>`;
}

function esc(s: any): string {
  return String(s || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

// ── Send ──────────────────────────────────────────────────────────────────────

async function sendEmail(to: any, subject: string, html: string) {
  const transport = createTransport();
  await transport.sendMail({ from: senderAddress(), to, subject, html });
}

async function sendTestEmail(to: any) {
  const html = `<!DOCTYPE html>
<html><body style="font-family:sans-serif;padding:32px;">
  <h2 style="color:#4f46e5;">Bill Tracker — Test Email</h2>
  <p>Your SMTP configuration is working correctly.</p>
  <p style="color:#6b7280;font-size:12px;">Sent at ${new Date().toISOString()}</p>
</body></html>`;
  try {
    await sendEmail(to, 'Bill Tracker — Test Email', html);
    markNotificationTestSuccess();
  } catch (err) {
    markNotificationError(err);
    throw err;
  }
}

// ── Notification tracking ─────────────────────────────────────────────────────

function hasNotification(
  db: Db,
  billId: any,
  userId: any,
  year: number,
  month: number,
  type: string,
  date: string,
): boolean {
  return !!db
    .prepare(
      `
    SELECT 1 FROM notifications
    WHERE bill_id=? AND user_id=? AND year=? AND month=? AND type=? AND sent_date=?
  `,
    )
    .get(billId, userId, year, month, type, date);
}

function recordNotification(
  db: Db,
  billId: any,
  userId: any,
  year: number,
  month: number,
  type: string,
  date: string,
): void {
  try {
    db.prepare(
      `
      INSERT INTO notifications (bill_id, user_id, year, month, type, sent_date)
      VALUES (?, ?, ?, ?, ?, ?)
    `,
    ).run(billId, userId, year, month, type, date);
  } catch {
    // Unique constraint: already recorded, ignore
  }
}

// ── Main notification runner (called by daily worker) ─────────────────────────

async function runNotifications() {
  const db: Db = getDb();

  const emailReady =
    getSetting('notify_smtp_enabled') === 'true' &&
    !!getSetting('notify_smtp_host') &&
    !!getSetting('notify_sender_address');

  // Check whether any user has push notifications enabled
  const pushReady = !!db
    .prepare('SELECT 1 FROM users WHERE notify_push_enabled=1 AND push_channel IS NOT NULL LIMIT 1')
    .get();

  // Nothing to do if neither email nor push is configured
  if (!emailReady && !pushReady) return;

  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth() + 1;
  const today = localDateString(now);

  const { getCycleRange, resolveDueDate } = require('./statusService.cts');

  const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all();
  const allowUserConfig = getSetting('notify_allow_user_config') === 'true';
  const globalRecipient = getSetting('notify_global_recipient');

  // Gather recipient list
  const recipients: any[] = [];

  if (allowUserConfig) {
    const users = db
      .prepare(
        "SELECT * FROM users WHERE active = 1 AND role='user' AND notifications_enabled=1 AND (notification_email IS NOT NULL AND notification_email != '' OR notify_push_enabled=1)",
      )
      .all();
    recipients.push(...users);
  } else if (globalRecipient) {
    recipients.push({
      id: 0,
      notification_email: globalRecipient,
      notify_3d: 1,
      notify_1d: 1,
      notify_due: 1,
      notify_overdue: 1,
    });
  }

  if (!recipients.length) return;

  const errors: any[] = [];

  const billIds = bills.map((b: Row) => b.id);
  const monthStart = `${year}-${String(month).padStart(2, '0')}-01`;
  const monthEnd = localDateString(new Date(year, month, 0));
  const paidMap = new Map<any, any>();
  if (billIds.length > 0) {
    const placeholders = billIds.map(() => '?').join(',');
    const paidRows = db
      .prepare(
        `
      SELECT bill_id, SUM(amount) AS paid_sum
      FROM payments
      WHERE bill_id IN (${placeholders})
        AND paid_date BETWEEN ? AND ?
        AND deleted_at IS NULL
        AND ${accountingActiveSql()}
      GROUP BY bill_id
    `,
      )
      .all(...billIds, monthStart, monthEnd);
    for (const row of paidRows) paidMap.set(row.bill_id, row.paid_sum);
  }

  const sentRows = db
    .prepare(
      `
    SELECT bill_id, user_id, type FROM notifications
    WHERE year = ? AND month = ? AND sent_date = ?
  `,
    )
    .all(year, month, today);
  const sentSet = new Set<string>(sentRows.map((n: Row) => `${n.bill_id}:${n.user_id}:${n.type}`));

  for (const bill of bills) {
    const dueDate = resolveDueDate(bill, year, month);
    if (!dueDate) continue;

    const totalPaid = paidMap.get(bill.id) ?? 0;
    const isPaid = totalPaid >= bill.expected_amount;
    if (isPaid) continue;

    const due = new Date(dueDate + 'T00:00:00');
    const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const dueDay = new Date(due.getFullYear(), due.getMonth(), due.getDate());
    const diffDays = Math.round((dueDay.getTime() - todayDate.getTime()) / 86400000);

    const type = reminderTypeFor(bill, diffDays);
    if (!type) continue;

    if (!bill.user_id) {
      console.warn(
        `[notifications] Bill id=${bill.id} name="${bill.name}" has no user_id — skipping`,
      );
      continue;
    }

    for (const recipient of recipients) {
      if (allowUserConfig && bill.user_id !== recipient.id) continue;

      if (type === 'due_3d' && !recipient.notify_3d) continue;
      if (type === 'due_1d' && !recipient.notify_1d) continue;
      if (type === 'due_today' && !recipient.notify_due) continue;
      if (type === 'overdue' && !recipient.notify_overdue) continue;

      if (sentSet.has(`${bill.id}:${recipient.id}:${type}`)) continue;

      const meta = TYPE_META[type];
      const subject = meta.subject(bill);
      const urgency = meta.urgency;
      const amount = '$' + (fromCents(bill.expected_amount) || 0).toFixed(2);
      const pushBody = `${subject} · ${amount}`;
      let sent = false;

      // Email
      if (recipient.notification_email) {
        const html = buildEmailHtml(bill, type, dueDate);
        try {
          await sendEmail(recipient.notification_email, subject, html);
          sent = true;
        } catch (err: any) {
          errors.push(`email/${recipient.notification_email}/${bill.name}: ${err.message}`);
        }
      }

      // Push (ntfy / Gotify / Discord / Telegram)
      if (recipient.notify_push_enabled && recipient.push_channel && recipient.push_url) {
        try {
          await sendPushToUser(recipient, subject, pushBody, urgency);
          sent = true;
        } catch (err: any) {
          errors.push(`push/${recipient.push_channel}/${bill.name}: ${err.message}`);
        }
      }

      if (sent) {
        recordNotification(db, bill.id, recipient.id, year, month, type, today);
        sentSet.add(`${bill.id}:${recipient.id}:${type}`);
      }
    }
  }

  if (errors.length) {
    markNotificationError(new Error(errors.join('; ')));
    console.error('[notifications] Send errors:', errors);
  } else {
    markNotificationSuccess();
    console.log(`[notifications] Run complete for ${today}`);
  }
}

// ── Drift / price-change digest email ────────────────────────────────────────

function fmtAmt(n: any): string {
  return '$' + Number(n || 0).toFixed(2);
}

function buildDriftDigestHtml(bills: any[]): string {
  const color = '#d97706'; // amber-600

  const rows = bills
    .map((b: Row) => {
      const sign = b.direction === 'up' ? '+' : '';
      const arrow = b.direction === 'up' ? '▲' : '▼';
      const arrowColor = b.direction === 'up' ? '#d97706' : '#0d9488';
      return `
      <tr>
        <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:14px;">${esc(b.name)}</td>
        <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:14px;font-family:monospace;text-decoration:line-through;color:#9ca3af;">${fmtAmt(b.expected_amount)}</td>
        <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:14px;font-family:monospace;font-weight:600;">${fmtAmt(b.recent_amount)}</td>
        <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:13px;color:${arrowColor};font-weight:700;">${arrow} ${sign}${b.drift_pct}%</td>
      </tr>`;
    })
    .join('');

  return `<!DOCTYPE html>
<html>
<body style="margin:0;padding:0;background:#f5f6fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:32px 16px;">
  <tr><td align="center">
    <table width="540" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;border:1px solid #e2e4eb;overflow:hidden;">
      <tr>
        <td style="background:${color};padding:16px 24px;">
          <p style="margin:0;color:#fff;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Bill Tracker</p>
          <h1 style="margin:4px 0 0;color:#fff;font-size:20px;font-weight:700;">Price Changes Detected</h1>
        </td>
      </tr>
      <tr>
        <td style="padding:20px 24px 8px;">
          <p style="margin:0;color:#374151;font-size:14px;">
            The following bills have been paid at amounts different from their expected amounts for the past ${bills[0]?.months_sampled ?? 2}+ months.
          </p>
        </td>
      </tr>
      <tr>
        <td style="padding:8px 24px 20px;">
          <table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;">
            <thead>
              <tr style="background:#fef9c3;">
                <th style="padding:8px 12px;text-align:left;font-size:12px;color:#92400e;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">Bill</th>
                <th style="padding:8px 12px;text-align:left;font-size:12px;color:#92400e;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">Was</th>
                <th style="padding:8px 12px;text-align:left;font-size:12px;color:#92400e;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">Now ~</th>
                <th style="padding:8px 12px;text-align:left;font-size:12px;color:#92400e;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">Change</th>
              </tr>
            </thead>
            <tbody>${rows}</tbody>
          </table>
        </td>
      </tr>
      <tr>
        <td style="padding:0 24px 24px;">
          <p style="margin:0;color:#6b7280;font-size:13px;">
            You can update the expected amounts or dismiss these alerts in Bill Tracker.
          </p>
        </td>
      </tr>
    </table>
  </td></tr>
</table>
</body>
</html>`;
}

async function runDriftNotifications() {
  if (getSetting('notify_smtp_enabled') !== 'true') return;
  if (!getSetting('notify_smtp_host')) return;
  if (!getSetting('notify_sender_address')) return;

  const db: Db = getDb();
  const { getDriftReport } = require('./driftService.cts');
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth() + 1;
  const today = localDateString(now);

  const allowUserConfig = getSetting('notify_allow_user_config') === 'true';
  const globalRecipient = getSetting('notify_global_recipient');

  const recipients: any[] = [];

  if (allowUserConfig) {
    const users = db
      .prepare(
        "SELECT * FROM users WHERE active=1 AND role='user' AND notifications_enabled=1 AND notify_amount_change=1 AND notification_email IS NOT NULL AND notification_email != ''",
      )
      .all();
    recipients.push(...users);
  } else if (globalRecipient) {
    recipients.push({ id: 0, notification_email: globalRecipient, notify_amount_change: 1 });
  }

  for (const recipient of recipients) {
    try {
      const report = getDriftReport(recipient.id, now);
      const newBills = (report.bills || []).filter(
        (b: Row) => !hasNotification(db, b.id, recipient.id, year, month, 'amount_change', today),
      );
      if (!newBills.length) continue;

      await sendEmail(
        recipient.notification_email,
        'Price Change Alert: Your bill amounts have changed',
        buildDriftDigestHtml(newBills),
      );

      for (const b of newBills) {
        recordNotification(db, b.id, recipient.id, year, month, 'amount_change', today);
      }
    } catch (err: any) {
      console.error(
        '[drift notifications] Error for recipient',
        recipient.notification_email,
        ':',
        err.message,
      );
    }
  }
}

module.exports = { runNotifications, runDriftNotifications, sendTestEmail, createTransport };
// Push helpers, exposed for the test-push route + tests (QA-B10-01: assigned AFTER the export above).
module.exports._push = {
  sendNtfy,
  sendGotify,
  sendDiscord,
  sendTelegram,
  sendTestPush,
  sendPushToUser,
  encryptSecret,
};
module.exports._email = { buildEmailHtml, esc };
module.exports._reminders = { reminderTypeFor, leadDaysOf, TYPE_META };
