'use strict';

/**
 * SIM 卡短信读取器
 * - 扫描 /dev/serial/by-path 中以 :1.3-port0 结尾的 Quectel AT 串口
 * - 单次执行：读取并覆盖写入 /home/sts/sms-messages.txt
 * - 不去重
 * - 显示短信真实时间（取自 +CMGL 头）
 * - 只保留最近 15 分钟内的短信
 * - 多卡安全长短信拼接：按 IMSI + From + timestampRaw 合并相邻分片
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const BY_PATH_DIR = '/dev/serial/by-path';
const TARGET_SUFFIX = ':1.3-port0';
const OUT_FILE = '/home/sts/sms-messages.txt';
const BAUD = 9600;
const WINDOW_MINUTES = 15;

const sleep = ms => new Promise(r => setTimeout(r, ms));

function setupPort(dev) {
  execSync(
    `stty -F "${dev}" ${BAUD} cs8 -cstopb -parenb cread clocal -crtscts raw ` +
    `-echo -echoe -echok -echonl min 0 time 5`,
    { stdio: 'ignore' }
  );
}

async function readRaw(fd, timeoutMs = 2000, stopOnOk = true) {
  const buf = Buffer.alloc(4096);
  const deadline = Date.now() + timeoutMs;
  let text = '';

  while (Date.now() < deadline) {
    let n = 0;
    try {
      n = fs.readSync(fd, buf, 0, buf.length, null);
    } catch (e) {
      if (e.code !== 'EAGAIN' && e.code !== 'EWOULDBLOCK') throw e;
    }

    if (n > 0) {
      text += buf.slice(0, n).toString('utf8');
      if (stopOnOk && (/\nOK|\rOK|ERROR/.test(text))) {
        await sleep(120);
        try {
          while (true) {
            const n2 = fs.readSync(fd, buf, 0, buf.length, null);
            if (n2 <= 0) break;
            text += buf.slice(0, n2).toString('utf8');
          }
        } catch (_) {}
        break;
      }
    } else {
      await sleep(50);
    }
  }
  return text;
}

async function atCmd(fd, command, waitMs = 2000) {
  try {
    while (fs.readSync(fd, Buffer.alloc(256), 0, 256, null) > 0) {}
  } catch (_) {}
  fs.writeSync(fd, Buffer.from(command + '\r'));
  const raw = await readRaw(fd, waitMs, true);
  return cleanLines(raw);
}

function cleanLines(raw) {
  return raw
    .replace(/\r/g, '\n')
    .split('\n')
    .map(l => l.trim())
    .filter(l => {
      if (!l) return false;
      if (l === 'OK' || l === 'ERROR') return false;
      if (l.startsWith('AT')) return false;
      const printable = [...l].filter(c => c >= ' ').length;
      return printable / Math.max(1, l.length) >= 0.85;
    });
}

function discoverPorts() {
  if (!fs.existsSync(BY_PATH_DIR)) return [];
  return fs.readdirSync(BY_PATH_DIR)
    .filter(n => n.endsWith(TARGET_SUFFIX))
    .sort()
    .map(n => {
      const link = path.join(BY_PATH_DIR, n);
      try {
        const real = fs.realpathSync(link);
        return fs.existsSync(real) ? { link, real } : null;
      } catch (_) {
        return null;
      }
    })
    .filter(Boolean);
}

function maybeDecodeUcs2(text) {
  const s = (text || '').replace(/\s+/g, '');
  if (!s) return '';
  if (/^\d+$/.test(s)) return '';
  if (s.length % 4 !== 0) return '';
  if (!/^[0-9A-Fa-f]+$/.test(s)) return '';
  try {
    return Buffer.from(s, 'hex').swap16().toString('utf16le').trim();
  } catch (_) {
    return '';
  }
}

function extractOtp(text) {
  const m = (text || '').match(/(?<!\d)(\d{4,8})(?!\d)/);
  return m ? m[1] : '';
}

function parseSmsTimestamp(raw) {
  if (!raw) return null;
  const m = raw.match(/^(\d{2})\/(\d{2})\/(\d{2}),(\d{2}):(\d{2}):(\d{2})([+-]\d{2})$/);
  if (!m) return null;

  const [, yy, mo, dd, hh, mi, ss, tzq] = m;
  const year = 2000 + Number(yy);
  const offsetMinutes = Number(tzq) * 15;

  const sign = offsetMinutes >= 0 ? '+' : '-';
  const abs = Math.abs(offsetMinutes);
  const offH = String(Math.floor(abs / 60)).padStart(2, '0');
  const offM = String(abs % 60).padStart(2, '0');

  const iso = `${year}-${mo}-${dd}T${hh}:${mi}:${ss}${sign}${offH}:${offM}`;
  const d = new Date(iso);
  return Number.isNaN(d.getTime()) ? null : d;
}

function formatDate(d) {
  if (!d || Number.isNaN(d.getTime())) return 'UNKNOWN';
  const pad = n => String(n).padStart(2, '0');
  const y = d.getFullYear();
  const mo = pad(d.getMonth() + 1);
  const da = pad(d.getDate());
  const hh = pad(d.getHours());
  const mi = pad(d.getMinutes());
  const ss = pad(d.getSeconds());

  const offset = -d.getTimezoneOffset();
  const sign = offset >= 0 ? '+' : '-';
  const abs = Math.abs(offset);
  const offH = pad(Math.floor(abs / 60));
  const offM = pad(abs % 60);

  return `${y}-${mo}-${da} ${hh}:${mi}:${ss} ${sign}${offH}${offM}`;
}

function recentOnly(messages, minutes = WINDOW_MINUTES) {
  const cutoff = Date.now() - minutes * 60 * 1000;
  return messages.filter(msg => {
    if (!msg.timestamp || Number.isNaN(msg.timestamp.getTime())) return false;
    return msg.timestamp.getTime() >= cutoff;
  });
}

function parseSms(lines) {
  const messages = [];
  let current = null;

  for (const line of lines) {
    if (line.startsWith('+CMGL:')) {
      const m = line.match(/^\+CMGL:\s*\d+,"[^"]*","([^"]*)",,"(\d{2}\/\d{2}\/\d{2},\d{2}:\d{2}:\d{2}[+-]\d{2})"$/);
      const sender = m ? (m[1] || 'UNKNOWN') : 'UNKNOWN';
      const timestampRaw = m ? m[2] : '';
      current = {
        sender,
        timestampRaw,
        timestamp: parseSmsTimestamp(timestampRaw),
        content: '',
        decoded: '',
        code: '',
      };
      messages.push(current);
      continue;
    }

    if (line.startsWith('+CMGR:')) {
      const parts = line.split(',').map(p => p.trim().replace(/^\"|\"$/g, ''));
      const sender = parts[1] || 'UNKNOWN';
      current = {
        sender,
        timestampRaw: '',
        timestamp: null,
        content: '',
        decoded: '',
        code: '',
      };
      messages.push(current);
      continue;
    }

    if (current && !line.startsWith('+')) {
      const raw = (current.content + ' ' + line).trim();
      current.content = raw;
      current.decoded = maybeDecodeUcs2(raw);
      current.code = extractOtp(current.decoded || current.content);
      current = null;
    }
  }

  return messages;
}

function mergeLongSms(imsi, messages) {
  const merged = [];

  for (const msg of messages) {
    const prev = merged[merged.length - 1];
    const sameGroup =
      prev &&
      (prev._mergeImsi === imsi) &&
      (prev.sender === msg.sender) &&
      (prev.timestampRaw === msg.timestampRaw);

    if (!sameGroup) {
      merged.push({
        ...msg,
        _mergeImsi: imsi,
      });
      continue;
    }

    prev.content = [prev.content, msg.content].filter(Boolean).join('');
    const prevText = prev.decoded || prev.content || '';
    const nextText = msg.decoded || msg.content || '';
    const joinedText = [prevText, nextText].filter(Boolean).join('');

    // 合并后优先重新按整体判断是否可解码；否则用拼接后的文本
    const redecoded = maybeDecodeUcs2((prev.content || ''));
    prev.decoded = redecoded || joinedText;
    prev.code = extractOtp(prev.decoded || prev.content);
  }

  return merged.map(({ _mergeImsi, ...rest }) => rest);
}

async function readPort({ link, real }) {
  let fd;
  try {
    setupPort(real);
    fd = fs.openSync(real, fs.constants.O_RDWR | fs.constants.O_NOCTTY | fs.constants.O_NONBLOCK);
    await readRaw(fd, 400, false);

    const cimiLines = await atCmd(fd, 'AT+CIMI');
    const imsiMatch = cimiLines.join('\n').match(/\b(\d{15})\b/);
    const imsi = imsiMatch ? imsiMatch[1] : 'UNKNOWN';

    await atCmd(fd, 'AT+CMGF=1');
    const smsLines = await atCmd(fd, 'AT+CMGL="ALL"', 4000);

    const parsed = parseSms(smsLines);
    const merged = mergeLongSms(imsi, parsed);
    const messages = recentOnly(merged, WINDOW_MINUTES);

    return { link, device: real, imsi, messages };
  } catch (e) {
    return { link, device: real, imsi: 'UNKNOWN', messages: [], error: e.message };
  } finally {
    if (fd !== undefined) {
      try { fs.closeSync(fd); } catch (_) {}
    }
  }
}

function render(results) {
  const now = new Date().toLocaleString('zh-CN', {
    hour12: false,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    timeZoneName: 'short',
  });

  const lines = [`=== ${now} ===`];

  if (!results.length) {
    lines.push('No matching ports found.');
    return lines.join('\n') + '\n';
  }

  let anySms = false;

  for (const item of results) {
    const errStr = item.error ? ` ERROR: ${item.error}` : '';
    lines.push(`Port: ${item.device} IMSI: ${item.imsi} Messages: ${item.messages.length}${errStr}`);

    for (const msg of item.messages) {
      anySms = true;
      lines.push(`SmsTime: ${formatDate(msg.timestamp)}`);
      lines.push(`IMSI: ${item.imsi}`);
      lines.push(`From: ${msg.sender}`);
      lines.push(`Content: ${msg.decoded || msg.content}`);
      if (msg.code) lines.push(`Code: ${msg.code}`);
      lines.push('');
    }
  }

  if (!anySms) lines.push(`No SMS found in last ${WINDOW_MINUTES} minutes.`);
  return lines.join('\n').trimEnd() + '\n';
}

async function pollSms() {
  const ports = discoverPorts();
  const results = await Promise.all(ports.map(readPort));
  const text = render(results);
  fs.writeFileSync(OUT_FILE, text, 'utf8');
  return text;
}

module.exports = { pollSms };

if (require.main === module) {
  pollSms()
    .then(text => process.stdout.write(text))
    .catch(err => {
      console.error(`sms-reader error: ${err.message}`);
      process.exitCode = 1;
    });
}
