'use strict';

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

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

type Row = Record<string, any>;

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

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

function ensureCalendarTokenSchema(db: Db = getDb()): void {
  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(): string {
  return crypto.randomBytes(32).toString('base64url');
}

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

function getActiveToken(userId: number, db: Db = getDb()): Row | null {
  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: number, label = 'Bill Tracker Calendar', db: Db = getDb()): Row {
  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: number, db: Db = getDb()): Row {
  return getActiveToken(userId, db) || createToken(userId, 'Bill Tracker Calendar', db);
}

function regenerateToken(userId: number, db: Db = getDb()): Row {
  ensureCalendarTokenSchema(db);
  let next: Row;
  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: number, db: Db = getDb()): void {
  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: unknown, db: Db = getDb()): Row | null {
  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: number, db: Db = getDb()): void {
  ensureCalendarTokenSchema(db);
  db.prepare("UPDATE calendar_tokens SET last_used_at = datetime('now') WHERE id = ?").run(tokenId);
}

function originFromRequest(req: Req): string {
  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: Req, token: string): string {
  return `${originFromRequest(req)}/api/calendar/feed.ics?token=${encodeURIComponent(token)}`;
}

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

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

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

function addDays(dateString: unknown, days: number): string {
  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: Date, months: number): Date {
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
}

function monthIterator(startDate: Date, endDate: Date): { year: number; month: number }[] {
  const months: { year: number; month: number }[] = [];
  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: unknown): string {
  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: unknown): string {
  const value = String(line);
  if (Buffer.byteLength(value, 'utf8') <= 75) return value;

  const lines: string[] = [];
  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: unknown): string {
  const normalized = String(value || 'monday')
    .trim()
    .toLowerCase();
  return WEEKDAY_CODES[normalized] || 'MO';
}

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

function dayForBill(bill: Row): number {
  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: Row = {}): string {
  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: Row, dueDate: string): string {
  return `bill-tracker-bill-${bill.id}-${dueDate}@bill-tracker`;
}

function eventSummary(bill: Row, detailLevel = 'standard'): string {
  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: Row, dueDate: string, detailLevel = 'standard'): string {
  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(),
}: {
  bill: Row;
  dueDate: string;
  detailLevel?: string;
  dtstamp?: string;
}): string[] {
  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: number, db: Db = getDb()): Row[] {
  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: number, options: Row = {}, db: Db = getDb()): any[] {
  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: any[] = [];
  const seen = new Set<string>();

  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 = [],
}: { name?: string; events?: any[] } = {}): string {
  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: number, options: Row = {}, db: Db = getDb()) {
  const events = buildFeedEvents(userId, options, db);
  return {
    ics: buildIcsCalendar({ name: options.name || 'Bill Tracker', events }),
    events,
  };
}

function previewFeed(userId: number, options: Row = {}, db: 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,
};
