import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { createServer } from "http";
import { parse as parseUrl } from "url";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { createHmac } from "crypto";
import { execFileSync } from "child_process";
import {
  processIncomingResponse,
  loadSMSConfig,
  cleanupExpiredPrompts,
  sendNotification
} from "./sms-notify.js";
import {
  queueAction,
  executeActionSafe,
  cleanupOldActions
} from "./sms-action-runner.js";
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
import {
  logWebhookRequest,
  logRateLimit,
  logSignatureInvalid,
  logBodyTooLarge,
  logContentTypeInvalid,
  logActionAllowed,
  logActionBlocked,
  logCleanup
} from "./security-logger.js";
const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
const MAX_SMS_BODY_LENGTH = 1e3;
const MAX_PHONE_LENGTH = 20;
const MAX_BODY_SIZE = 50 * 1024;
const RATE_LIMIT_WINDOW_MS = 60 * 1e3;
const RATE_LIMIT_MAX_REQUESTS = 30;
const rateLimitStore = /* @__PURE__ */ new Map();
function checkRateLimit(ip) {
  const now = Date.now();
  const record = rateLimitStore.get(ip);
  if (!record || now > record.resetTime) {
    rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
    return true;
  }
  if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
    return false;
  }
  record.count++;
  return true;
}
function verifyTwilioSignature(url, params, signature) {
  const authToken = process.env["TWILIO_AUTH_TOKEN"];
  if (!authToken) {
    console.warn(
      "[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification"
    );
    return true;
  }
  const sortedKeys = Object.keys(params).sort();
  let data = url;
  for (const key of sortedKeys) {
    data += key + params[key];
  }
  const hmac = createHmac("sha1", authToken);
  hmac.update(data);
  const expectedSignature = hmac.digest("base64");
  return signature === expectedSignature;
}
function parseFormData(body) {
  const params = new URLSearchParams(body);
  const result = {};
  params.forEach((value, key) => {
    result[key] = value;
  });
  return result;
}
function storeLatestResponse(promptId, response, action) {
  ensureSecureDir(join(homedir(), ".stackmemory"));
  const responsePath = join(
    homedir(),
    ".stackmemory",
    "sms-latest-response.json"
  );
  writeFileSecure(
    responsePath,
    JSON.stringify({
      promptId,
      response,
      action,
      timestamp: (/* @__PURE__ */ new Date()).toISOString()
    })
  );
}
function storeIncomingRequest(from, message) {
  ensureSecureDir(join(homedir(), ".stackmemory"));
  const requestPath = join(
    homedir(),
    ".stackmemory",
    "sms-incoming-request.json"
  );
  writeFileSecure(
    requestPath,
    JSON.stringify({
      from,
      message,
      timestamp: (/* @__PURE__ */ new Date()).toISOString(),
      processed: false
    })
  );
}
function getIncomingRequest() {
  const requestPath = join(
    homedir(),
    ".stackmemory",
    "sms-incoming-request.json"
  );
  if (!existsSync(requestPath)) {
    return null;
  }
  try {
    const data = JSON.parse(readFileSync(requestPath, "utf-8"));
    if (data.processed) {
      return null;
    }
    return data;
  } catch {
    return null;
  }
}
function markRequestProcessed() {
  const requestPath = join(
    homedir(),
    ".stackmemory",
    "sms-incoming-request.json"
  );
  if (!existsSync(requestPath)) {
    return;
  }
  try {
    const data = JSON.parse(readFileSync(requestPath, "utf-8"));
    data.processed = true;
    writeFileSecure(requestPath, JSON.stringify(data));
  } catch {
  }
}
async function handleSMSWebhook(payload) {
  const { From, Body } = payload;
  if (Body && Body.length > MAX_SMS_BODY_LENGTH) {
    console.log(`[sms-webhook] Body too long: ${Body.length} chars`);
    return { response: "Message too long. Max 1000 characters." };
  }
  if (From && From.length > MAX_PHONE_LENGTH) {
    console.log(`[sms-webhook] Invalid phone number length`);
    return { response: "Invalid phone number." };
  }
  console.log(`[sms-webhook] Received from ${From}: ${Body}`);
  const result = processIncomingResponse(From, Body);
  if (!result.matched) {
    if (result.prompt) {
      return {
        response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(", ")}`
      };
    }
    storeIncomingRequest(From, Body);
    console.log(
      `[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`
    );
    return { response: "Got it! Your request has been queued." };
  }
  storeLatestResponse(
    result.prompt?.id || "unknown",
    result.response || Body,
    result.action
  );
  triggerResponseNotification(result.response || Body);
  if (result.action) {
    console.log(`[sms-webhook] Executing action: ${result.action}`);
    const actionResult = await executeActionSafe(
      result.action,
      result.response || Body
    );
    if (actionResult.success) {
      logActionAllowed("sms-webhook", result.action);
      console.log(
        `[sms-webhook] Action completed: ${(actionResult.output || "").substring(0, 200)}`
      );
      return {
        response: `Done! Action executed successfully.`,
        action: result.action,
        queued: false
      };
    } else {
      logActionBlocked(
        "sms-webhook",
        result.action,
        actionResult.error || "unknown"
      );
      console.log(`[sms-webhook] Action failed: ${actionResult.error}`);
      queueAction(
        result.prompt?.id || "unknown",
        result.response || Body,
        result.action
      );
      return {
        response: `Action failed, queued for retry: ${(actionResult.error || "").substring(0, 50)}`,
        action: result.action,
        queued: true
      };
    }
  }
  return {
    response: `Received: ${result.response}. Next action will be triggered.`
  };
}
function escapeAppleScript(str) {
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").substring(0, 200);
}
function triggerResponseNotification(response) {
  const safeMessage = escapeAppleScript(`SMS Response: ${response}`);
  try {
    execFileSync(
      "osascript",
      [
        "-e",
        `display notification "${safeMessage}" with title "StackMemory" sound name "Glass"`
      ],
      { stdio: "ignore", timeout: 5e3 }
    );
  } catch {
  }
  try {
    const signalPath = join(homedir(), ".stackmemory", "sms-signal.txt");
    writeFileSecure(
      signalPath,
      JSON.stringify({
        type: "sms_response",
        response,
        timestamp: (/* @__PURE__ */ new Date()).toISOString()
      })
    );
  } catch {
  }
  console.log(`
*** SMS RESPONSE RECEIVED: "${response}" ***`);
  console.log(`*** Run: stackmemory notify run-actions ***
`);
}
function twimlResponse(message) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>${escapeXml(message)}</Message>
</Response>`;
}
function escapeXml(str) {
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
function startWebhookServer(port = 3456) {
  const server = createServer(
    async (req, res) => {
      const url = parseUrl(req.url || "/", true);
      if (url.pathname === "/health") {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ status: "ok" }));
        return;
      }
      if ((url.pathname === "/sms" || url.pathname === "/sms/incoming" || url.pathname === "/webhook") && req.method === "POST") {
        const clientIp = req.socket.remoteAddress || "unknown";
        logWebhookRequest(
          "sms-webhook",
          req.method || "POST",
          url.pathname || "/sms",
          clientIp
        );
        if (!checkRateLimit(clientIp)) {
          logRateLimit("sms-webhook", clientIp);
          res.writeHead(429, {
            "Content-Type": "text/xml",
            "Retry-After": "60"
          });
          res.end(twimlResponse("Too many requests. Please try again later."));
          return;
        }
        const contentType = req.headers["content-type"] || "";
        if (!contentType.includes("application/x-www-form-urlencoded")) {
          logContentTypeInvalid("sms-webhook", contentType, clientIp);
          res.writeHead(400, { "Content-Type": "text/xml" });
          res.end(twimlResponse("Invalid content type"));
          return;
        }
        let body = "";
        let bodyTooLarge = false;
        req.on("data", (chunk) => {
          body += chunk;
          if (body.length > MAX_BODY_SIZE) {
            bodyTooLarge = true;
            logBodyTooLarge("sms-webhook", body.length, clientIp);
            req.destroy();
          }
        });
        req.on("end", async () => {
          if (bodyTooLarge) {
            res.writeHead(413, { "Content-Type": "text/xml" });
            res.end(twimlResponse("Request too large"));
            return;
          }
          try {
            const payload = parseFormData(
              body
            );
            const twilioSignature = req.headers["x-twilio-signature"];
            const webhookUrl = `${req.headers["x-forwarded-proto"] || "http"}://${req.headers.host}${req.url}`;
            if (twilioSignature && !verifyTwilioSignature(
              webhookUrl,
              payload,
              twilioSignature
            )) {
              logSignatureInvalid("sms-webhook", clientIp);
              console.error("[sms-webhook] Invalid Twilio signature");
              res.writeHead(401, { "Content-Type": "text/xml" });
              res.end(twimlResponse("Unauthorized"));
              return;
            }
            const result = await handleSMSWebhook(payload);
            res.writeHead(200, { "Content-Type": "text/xml" });
            res.end(twimlResponse(result.response));
          } catch (err) {
            console.error("[sms-webhook] Error:", err);
            res.writeHead(500, { "Content-Type": "text/xml" });
            res.end(twimlResponse("Error processing message"));
          }
        });
        return;
      }
      if (url.pathname === "/sms/status" && req.method === "POST") {
        let body = "";
        req.on("data", (chunk) => {
          body += chunk;
        });
        req.on("end", () => {
          try {
            const payload = parseFormData(body);
            console.log(
              `[sms-webhook] Status update: ${payload["MessageSid"]} -> ${payload["MessageStatus"]}`
            );
            const statusPath = join(
              homedir(),
              ".stackmemory",
              "sms-status.json"
            );
            const statuses = existsSync(statusPath) ? JSON.parse(readFileSync(statusPath, "utf8")) : {};
            statuses[payload["MessageSid"]] = payload["MessageStatus"];
            writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));
            res.writeHead(200, { "Content-Type": "text/plain" });
            res.end("OK");
          } catch (err) {
            console.error("[sms-webhook] Status error:", err);
            res.writeHead(500);
            res.end("Error");
          }
        });
        return;
      }
      if (url.pathname === "/status") {
        const config = loadSMSConfig();
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(
          JSON.stringify({
            enabled: config.enabled,
            pendingPrompts: config.pendingPrompts.length
          })
        );
        return;
      }
      if (url.pathname === "/request" && req.method === "GET") {
        const request = getIncomingRequest();
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ request }));
        return;
      }
      if (url.pathname === "/request/ack" && req.method === "POST") {
        markRequestProcessed();
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ success: true }));
        return;
      }
      if (url.pathname === "/send" && req.method === "POST") {
        let body = "";
        req.on("data", (chunk) => {
          body += chunk;
          if (body.length > MAX_BODY_SIZE) {
            req.destroy();
          }
        });
        req.on("end", async () => {
          try {
            const payload = JSON.parse(body);
            const message = payload.message || payload.body || "";
            const title = payload.title || "Notification";
            const type = payload.type || "custom";
            if (!message) {
              res.writeHead(400, { "Content-Type": "application/json" });
              res.end(
                JSON.stringify({ success: false, error: "Message required" })
              );
              return;
            }
            const result = await sendNotification({
              type,
              title,
              message
            });
            res.writeHead(result.success ? 200 : 500, {
              "Content-Type": "application/json"
            });
            res.end(JSON.stringify(result));
          } catch (err) {
            console.error("[sms-webhook] Send error:", err);
            res.writeHead(500, { "Content-Type": "application/json" });
            res.end(
              JSON.stringify({
                success: false,
                error: err instanceof Error ? err.message : "Send failed"
              })
            );
          }
        });
        return;
      }
      res.writeHead(404);
      res.end("Not found");
    }
  );
  server.listen(port, () => {
    console.log(`[sms-webhook] Server listening on port ${port}`);
    console.log(
      `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`
    );
    console.log(
      `[sms-webhook] Status callback:   http://localhost:${port}/sms/status`
    );
    console.log(`[sms-webhook] Configure these URLs in Twilio console`);
    setInterval(() => {
      try {
        const expiredPrompts = cleanupExpiredPrompts();
        const oldActions = cleanupOldActions();
        if (expiredPrompts > 0 || oldActions > 0) {
          logCleanup("sms-webhook", expiredPrompts, oldActions);
          console.log(
            `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`
          );
        }
      } catch {
      }
    }, CLEANUP_INTERVAL_MS);
    console.log(
      `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1e3}s`
    );
  });
}
async function smsWebhookMiddleware(req, res) {
  const result = await handleSMSWebhook(req.body);
  res.type("text/xml");
  res.send(twimlResponse(result.response));
}
if (process.argv[1]?.endsWith("sms-webhook.js")) {
  const port = parseInt(process.env["SMS_WEBHOOK_PORT"] || "3456", 10);
  startWebhookServer(port);
}
export {
  getIncomingRequest,
  handleSMSWebhook,
  markRequestProcessed,
  smsWebhookMiddleware,
  startWebhookServer
};
//# sourceMappingURL=sms-webhook.js.map
