'use strict';

const crypto = require('crypto');
const { getDb } = require('../db/database');
const { normalizeCycleType, resolveDueDate } = require('./statusService');
const { fromCents } = require('../utils/money.mts');

const PRODID = '-//Bill Tracker//Calendar Feed//EN';
const FEED_PAST_MONTHS = 12;
const FEED_FUTURE_MONTHS = 24;

const WEEKDAY_CODES = {
  sunday: 'SU',
  monday: 'MO',
  tuesday: 'TU',
  wednesday: 'WE',
  thursday: 'TH',
  friday: 'FR',
  saturday: 'SA',
};

function ensureCalendarTokenSchema(db = getDb()) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS calendar_tokens (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
      token TEXT NOT NULL UNIQUE,
      label TEXT,
      active INTEGER NOT NULL DEFAULT 1,
      last_used_at TEXT,
      created_at TEXT NOT NULL DEFAULT (datetime('now')),
      revoked_at TEXT
    );
    CREATE INDEX IF NOT EXISTS idx_calendar_tokens_token ON calendar_tokens(token) WHERE active = 1;
    CREATE INDEX IF NOT EXISTS idx_calendar_tokens_user_active ON calendar_tokens(user_id, active);
  `);
}

function generateToken() {
  return crypto.randomBytes(32).toString('base64url');
}

function sanitizeLabel(label) {
  const value = String(label || 'Bill Tracker Calendar').trim();
  return value.slice(0, 80) || 'Bill Tracker Calendar';
}

function getActiveToken(userId, db = getDb()) {
  ensureCalendarTokenSchema(db);
  return db.prepare(`
    SELECT id, user_id, token, label, active, last_used_at, created_at, revoked_at
    FROM calendar_tokens
    WHERE user_id = ? AND active = 1
    ORDER BY created_at DESC, id DESC
    LIMIT 1
  `).get(userId) || null;
}

function createToken(userId, label = 'Bill Tracker Calendar', db = getDb()) {
  ensureCalendarTokenSchema(db);
  const token = generateToken();
  const result = db.prepare(`
    INSERT INTO calendar_tokens (user_id, token, label, active, created_at)
    VALUES (?, ?, ?, 1, datetime('now'))
  `).run(userId, token, sanitizeLabel(label));
  return db.prepare(`
    SELECT id, user_id, token, label, active, last_used_at, created_at, revoked_at
    FROM calendar_tokens
    WHERE id = ?
  `).get(result.lastInsertRowid);
}

function getOrCreateToken(userId, db = getDb()) {
  return getActiveToken(userId, db) || createToken(userId, 'Bill Tracker Calendar', db);
}

function regenerateToken(userId, db = getDb()) {
  ensureCalendarTokenSchema(db);
  let next;
  db.transaction(() => {
    db.prepare(`
      UPDATE calendar_tokens
      SET active = 0, revoked_at = datetime('now')
      WHERE user_id = ? AND active = 1
    `).run(userId);
    next = createToken(userId, 'Bill Tracker Calendar', db);
  })();
  return next;
}

function revokeToken(userId, db = getDb()) {
  ensureCalendarTokenSchema(db);
  db.prepare(`
    UPDATE calendar_tokens
    SET active = 0, revoked_at = datetime('now')
    WHERE user_id = ? AND active = 1
  `).run(userId);
}

function getTokenRecord(token, db = getDb()) {
  ensureCalendarTokenSchema(db);
  if (!token || typeof token !== 'string') return null;
  return db.prepare(`
    SELECT id, user_id, token, label, active, last_used_at, created_at, revoked_at
    FROM calendar_tokens
    WHERE token = ? AND active = 1
  `).get(token) || null;
}

function markTokenUsed(tokenId, db = getDb()) {
  ensureCalendarTokenSchema(db);
  db.prepare('UPDATE calendar_tokens SET last_used_at = datetime(\'now\') WHERE id = ?').run(tokenId);
}

function originFromRequest(req) {
  const host = req.get?.('x-forwarded-host') || req.get?.('host') || 'localhost';
  const proto = req.get?.('x-forwarded-proto') || req.protocol || 'http';
  return `${String(proto).split(',')[0]}://${String(host).split(',')[0]}`;
}

function feedUrlForToken(req, token) {
  return `${originFromRequest(req)}/api/calendar/feed.ics?token=${encodeURIComponent(token)}`;
}

function ymd(date) {
  return [
    date.getUTCFullYear(),
    String(date.getUTCMonth() + 1).padStart(2, '0'),
    String(date.getUTCDate()).padStart(2, '0'),
  ].join('-');
}

function icsDate(dateString) {
  return String(dateString).replaceAll('-', '');
}

function utcStamp(date = new Date()) {
  return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
}

function addDays(dateString, days) {
  const [year, month, day] = String(dateString).split('-').map(Number);
  const date = new Date(Date.UTC(year, month - 1, day));
  date.setUTCDate(date.getUTCDate() + days);
  return ymd(date);
}

function addMonths(date, months) {
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
}

function monthIterator(startDate, endDate) {
  const months = [];
  let cursor = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), 1));
  const end = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), 1));
  while (cursor <= end) {
    months.push({ year: cursor.getUTCFullYear(), month: cursor.getUTCMonth() + 1 });
    cursor = addMonths(cursor, 1);
  }
  return months;
}

// RFC 5545 §3.3.11 TEXT: escape backslash, semicolon, comma, and line breaks.
function escapeText(value) {
  return String(value ?? '')
    .replace(/\\/g, '\\\\')
    .replace(/\r\n|\r|\n/g, '\\n')
    .replace(/;/g, '\\;')
    .replace(/,/g, '\\,');
}

// RFC 5545 §3.1 content lines: fold at 75 octets, not JavaScript characters.
function foldLine(line) {
  const value = String(line);
  if (Buffer.byteLength(value, 'utf8') <= 75) return value;

  const lines = [];
  let current = '';
  let limit = 75;

  for (const char of value) {
    const next = current + char;
    if (Buffer.byteLength(next, 'utf8') > limit) {
      lines.push(current);
      current = char;
      limit = 74; // continuation lines include one leading WSP octet
    } else {
      current = next;
    }
  }

  if (current) lines.push(current);
  return lines.map((part, index) => (index === 0 ? part : ` ${part}`)).join('\r\n');
}

function weekdayForCycleDay(value) {
  const normalized = String(value || 'monday').trim().toLowerCase();
  return WEEKDAY_CODES[normalized] || 'MO';
}

function monthForCycleDay(value) {
  const parsed = parseInt(value, 10);
  return Number.isInteger(parsed) && parsed >= 1 && parsed <= 12 ? parsed : 1;
}

function dayForBill(bill) {
  const parsed = parseInt(bill?.due_day, 10);
  return Number.isInteger(parsed) && parsed >= 1 && parsed <= 31 ? parsed : 1;
}

// RRULE values follow RFC 5545 §3.3.10 and mirror the app's five cycle types.
function rruleForCycle(bill = {}) {
  const cycleType = normalizeCycleType(bill);
  if (cycleType === 'weekly') {
    return `FREQ=WEEKLY;BYDAY=${weekdayForCycleDay(bill.cycle_day)}`;
  }
  if (cycleType === 'biweekly') {
    return `FREQ=WEEKLY;INTERVAL=2;BYDAY=${weekdayForCycleDay(bill.cycle_day)}`;
  }
  if (cycleType === 'quarterly') {
    return `FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=${dayForBill(bill)}`;
  }
  if (cycleType === 'annual') {
    return `FREQ=YEARLY;BYMONTH=${monthForCycleDay(bill.cycle_day)};BYMONTHDAY=${dayForBill(bill)}`;
  }
  return `FREQ=MONTHLY;BYMONTHDAY=${dayForBill(bill)}`;
}

function eventUid(bill, dueDate) {
  return `bill-tracker-bill-${bill.id}-${dueDate}@bill-tracker`;
}

function eventSummary(bill, detailLevel = 'standard') {
  if (detailLevel === 'private') return 'Bill due';
  if (detailLevel === 'full') return `${bill.name} due - $${(fromCents(bill.expected_amount) || 0).toFixed(2)}`;
  return `${bill.name} due`;
}

function eventDescription(bill, dueDate, detailLevel = 'standard') {
  const lines = ['Bill Tracker reminder'];
  if (detailLevel !== 'private') lines.push(`Bill: ${bill.name}`);
  if (detailLevel === 'full') lines.push(`Expected amount: $${(fromCents(bill.expected_amount) || 0).toFixed(2)}`);
  lines.push(`Due date: ${dueDate}`);
  if (bill.category_name) lines.push(`Category: ${bill.category_name}`);
  if (bill.autopay_enabled) lines.push('Autopay: enabled');
  return lines.join('\n');
}

function buildEvent({ bill, dueDate, detailLevel = 'standard', dtstamp = utcStamp() }) {
  const dtEnd = addDays(dueDate, 1);
  const lines = [
    'BEGIN:VEVENT',
    `UID:${eventUid(bill, dueDate)}`,
    `DTSTAMP:${dtstamp}`,
    `DTSTART;VALUE=DATE:${icsDate(dueDate)}`,
    `DTEND;VALUE=DATE:${icsDate(dtEnd)}`,
    `SUMMARY:${escapeText(eventSummary(bill, detailLevel))}`,
    `DESCRIPTION:${escapeText(eventDescription(bill, dueDate, detailLevel))}`,
    `CATEGORIES:${escapeText(bill.category_name || 'Bills')}`,
    'TRANSP:TRANSPARENT',
    'STATUS:CONFIRMED',
    'END:VEVENT',
  ];
  return lines;
}

function loadActiveBillsForFeed(userId, db = getDb()) {
  return db.prepare(`
    SELECT b.*, c.name AS category_name
    FROM bills b
    LEFT JOIN categories c ON b.category_id = c.id AND c.deleted_at IS NULL
    WHERE b.user_id = ?
      AND b.active = 1
      AND b.deleted_at IS NULL
    ORDER BY b.name ASC
  `).all(userId);
}

function buildFeedEvents(userId, options = {}, db = getDb()) {
  const now = options.now || new Date();
  const start = options.startDate || addMonths(now, -(options.pastMonths ?? FEED_PAST_MONTHS));
  const end = options.endDate || addMonths(now, options.futureMonths ?? FEED_FUTURE_MONTHS);
  const detailLevel = options.detailLevel || 'standard';
  const dtstamp = options.dtstamp || utcStamp(now);
  const events = [];
  const seen = new Set();

  for (const bill of loadActiveBillsForFeed(userId, db)) {
    for (const { year, month } of monthIterator(start, end)) {
      const dueDate = resolveDueDate(bill, year, month);
      if (!dueDate) continue;
      const uid = eventUid(bill, dueDate);
      if (seen.has(uid)) continue;
      seen.add(uid);
      events.push({ bill, dueDate, uid, lines: buildEvent({ bill, dueDate, detailLevel, dtstamp }) });
    }
  }

  events.sort((a, b) => a.dueDate.localeCompare(b.dueDate) || a.bill.name.localeCompare(b.bill.name));
  return events;
}

function buildIcsCalendar({ name = 'Bill Tracker', events = [] } = {}) {
  const lines = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    `PRODID:${PRODID}`,
    'CALSCALE:GREGORIAN',
    'METHOD:PUBLISH',
    `X-WR-CALNAME:${escapeText(name)}`,
    'X-WR-TIMEZONE:UTC',
  ];

  for (const event of events) {
    lines.push(...event.lines);
  }

  lines.push('END:VCALENDAR');
  return `${lines.map(foldLine).join('\r\n')}\r\n`;
}

function buildCalendarFeed(userId, options = {}, db = getDb()) {
  const events = buildFeedEvents(userId, options, db);
  return {
    ics: buildIcsCalendar({ name: options.name || 'Bill Tracker', events }),
    events,
  };
}

function previewFeed(userId, options = {}, db = getDb()) {
  const events = buildFeedEvents(userId, { ...options, pastMonths: 0, futureMonths: 6 }, db);
  return events.slice(0, options.limit || 10).map(event => ({
    uid: event.uid,
    bill_id: event.bill.id,
    name: event.bill.name,
    due_date: event.dueDate,
    amount: fromCents(event.bill.expected_amount) || 0,
    cycle_type: normalizeCycleType(event.bill),
    category_name: event.bill.category_name || null,
  }));
}

module.exports = {
  buildCalendarFeed,
  buildFeedEvents,
  createToken,
  ensureCalendarTokenSchema,
  escapeText,
  feedUrlForToken,
  foldLine,
  getActiveToken,
  getOrCreateToken,
  getTokenRecord,
  markTokenUsed,
  previewFeed,
  regenerateToken,
  revokeToken,
  rruleForCycle,
};
