import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";

const port = Number.parseInt(process.env.PORT || "8080", 10);
const publicUrl = String(process.env.CARBON_FEEDBACK_PUBLIC_URL || `http://127.0.0.1:${port}`).replace(/\/$/, "");
const tokenSecret = String(process.env.CARBON_FEEDBACK_TOKEN_SECRET || "");
const apiKeys = new Set(String(process.env.CARBON_FEEDBACK_API_KEYS || "").split(",").map((item) => item.trim()).filter(Boolean));
const dataFile = path.resolve(process.env.CARBON_FEEDBACK_DATA_FILE || ".data/feedback.json");
const dev = process.env.CARBON_FEEDBACK_DEV === "true";
if (!dev && tokenSecret.length < 32) throw new Error("CARBON_FEEDBACK_TOKEN_SECRET must contain at least 32 characters.");
if (!dev && !apiKeys.size) throw new Error("CARBON_FEEDBACK_API_KEYS must contain at least one private client key.");
if (!dev && !publicUrl.startsWith("https://")) throw new Error("CARBON_FEEDBACK_PUBLIC_URL must be HTTPS outside development.");

const now = () => new Date().toISOString();
const hash = (value) => crypto.createHmac("sha256", tokenSecret || "development-only-secret").update(value).digest("base64url");
const random = () => crypto.randomBytes(32).toString("base64url");
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);

function load() {
  try { const result = JSON.parse(fs.readFileSync(dataFile, "utf8")); return result?.schema === "carbon.private-feedback-service/v1" ? result : { schema: "carbon.private-feedback-service/v1", reviews: [] }; }
  catch { return { schema: "carbon.private-feedback-service/v1", reviews: [] }; }
}
function save(state) { fs.mkdirSync(path.dirname(dataFile), { recursive: true, mode: 0o700 }); const temporary = `${dataFile}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 }); fs.renameSync(temporary, dataFile); fs.chmodSync(dataFile, 0o600); }
function text(value, field, max, required = false) { const result = String(value || "").trim(); if (required && !result) throw new Error(`${field} is required.`); if (result.length > max) throw new Error(`${field} must be ${max} characters or fewer.`); return result; }
function email(value) { const result = text(value, "recipient.email", 320); if (result && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(result)) throw new Error("recipient.email must be a valid email address."); return result.toLowerCase(); }
function safeArtifact(value) { const result = text(value, "artifact", 4096); if (!result) return result; if (!/^https?:\/\//i.test(result)) return result; let url; try { url = new URL(result); } catch { throw new Error("artifact URL must be valid HTTP(S), or use a local path/identifier displayed as text only."); } if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error("artifact URL must not contain embedded credentials."); for (const [key] of url.searchParams) if (/token|secret|password|auth|key|session/i.test(key)) throw new Error("artifact URL must not contain sensitive query parameters."); return result; }
function body(req, limit = 1_000_000) { return new Promise((resolve, reject) => { let raw = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { raw += chunk; if (raw.length > limit) { reject(new Error("Request too large.")); req.destroy(); } }); req.on("end", () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch { reject(new Error("Invalid JSON.")); } }); req.on("error", reject); }); }
function json(res, status, value) { res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff" }); res.end(JSON.stringify(value)); }
function requestToken(req, url) { return String(req.headers["x-carbon-feedback-token"] || url.searchParams.get("token") || ""); }
function find(state, id) { const review = state.reviews.find((item) => item.id === id); if (!review) throw Object.assign(new Error("Review not found."), { status: 404 }); if (review.expiresAt && Date.parse(review.expiresAt) <= Date.now() && !["closed", "expired"].includes(review.status)) review.status = "expired"; return review; }
function assertReviewer(req, url, review) { if (["closed", "expired", "revoked"].includes(review.status)) throw Object.assign(new Error(`Review is ${review.status}.`), { status: 410 }); if (!crypto.timingSafeEqual(Buffer.from(hash(requestToken(req, url))), Buffer.from(review.reviewerTokenHash))) throw Object.assign(new Error("Reviewer capability required."), { status: 401 }); }
function dto(review) { return { id: review.id, title: review.title, focus: review.focus, artifact: review.artifact, artifactKind: review.artifactKind, context: review.context, status: review.status, createdAt: review.createdAt, expiresAt: review.expiresAt, responseCount: review.responses.length, responses: review.responses.map(({ rating, category, response, createdAt }) => ({ rating, category, response, createdAt })) }; }
function mailHtml(review, reviewerUrl) { return `<!doctype html><html><body style="margin:0;background:#0b0d0e;color:#f0f1ec;font-family:Arial,sans-serif"><table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr><td style="padding:32px"><table role="presentation" width="100%" style="max-width:640px;background:#121516;border:1px solid #303638"><tr><td style="padding:30px"><div style="font-size:22px;font-weight:800;letter-spacing:5px">CARBON</div><p style="color:#a8aeae;font-size:11px;letter-spacing:2px;text-transform:uppercase">Private feedback request</p><h1 style="font-size:32px;line-height:1.05">${esc(review.title)}</h1><p style="font-size:16px;line-height:1.55">${esc(review.focus)}</p>${review.artifact ? `<p style="padding:14px;background:#0d1011;color:#a8aeae;word-break:break-word">${esc(review.artifact)}</p>` : ""}<p style="color:#a8aeae;line-height:1.5">Your response is returned to the requesting project. This invitation uses a private capability link; please do not forward it.</p><p style="margin:28px 0"><a href="${esc(reviewerUrl)}" style="display:inline-block;background:#6ee0b5;color:#07110d;padding:14px 18px;text-decoration:none;font-weight:800">Open private feedback workspace</a></p><p style="color:#a8aeae;font-size:12px">Email clients do not reliably support safe embedded forms. The button opens the responsive private feedback page.</p></td></tr></table></td></tr></table></body></html>`; }
async function deliverEmail(review, reviewerUrl) {
  const recipient = review.recipient?.email; if (!recipient) return { status: "queued", message: "No recipient email was supplied; the reviewer capability link is ready." };
  const subject = `CARBON feedback: ${review.title}`; const html = mailHtml(review, reviewerUrl);
  if (process.env.CARBON_FEEDBACK_MAIL_WEBHOOK_URL) { const response = await fetch(process.env.CARBON_FEEDBACK_MAIL_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ to: recipient, from: process.env.CARBON_FEEDBACK_MAIL_FROM || "", subject, html }) }); if (!response.ok) throw new Error("Private mail webhook rejected the invitation."); return { status: "sent", message: "Rich HTML invitation was sent through the private mail webhook." }; }
  if (process.env.SMTP_URL) { const nodemailer = await import("nodemailer"); const transporter = nodemailer.default.createTransport(process.env.SMTP_URL); await transporter.sendMail({ from: process.env.CARBON_FEEDBACK_MAIL_FROM || "CARBON Feedback <feedback@localhost>", to: recipient, subject, html, text: `CARBON feedback: ${review.title}\n\n${review.focus}\n\nOpen private workspace: ${reviewerUrl}` }); return { status: "sent", message: "Rich HTML invitation was sent through configured SMTP." }; }
  return { status: "queued", message: "Review queued. Configure SMTP_URL or CARBON_FEEDBACK_MAIL_WEBHOOK_URL to send the rich email invitation." };
}
function page() { return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CARBON Private Feedback</title><style>body{margin:0;background:#0b0d0e;color:#f0f1ec;font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.shell{max-width:860px;margin:auto;padding:24px}.card{margin-top:20px;background:#121516;border:1px solid #303638;padding:clamp(22px,5vw,48px)}.tag{color:#a8aeae;font-size:11px;letter-spacing:2px;text-transform:uppercase;font-weight:800}h1{font-size:clamp(37px,7vw,72px);line-height:.95;letter-spacing:-.06em;margin:11px 0 20px}p{color:#c9cecb}.artifact{color:#a8aeae;background:#0d1011;padding:14px;overflow-wrap:anywhere}.rate{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}.rate button,textarea,select{background:#0d1011;color:#f0f1ec;border:1px solid #303638;font:inherit}.rate button{padding:13px;cursor:pointer;font-weight:800}.rate button[aria-pressed=true]{background:#6ee0b5;color:#07110d;border-color:#6ee0b5}textarea,select{width:100%;padding:12px;box-sizing:border-box;margin-top:7px}textarea{min-height:130px}label{display:block;margin-top:18px;color:#a8aeae;font-size:11px;letter-spacing:1.5px;text-transform:uppercase;font-weight:800}button.primary{margin-top:18px;background:#6ee0b5;color:#07110d;border:0;padding:14px 18px;font-weight:850;cursor:pointer}.notice{min-height:24px;margin-top:12px;color:#6ee0b5}.muted{color:#a8aeae;font-size:13px}</style></head><body><main class="shell"><div class="tag">CARBON · Private reviewer workspace</div><section class="card"><div id="app">Loading protected request…</div></section></main><script>const token=location.hash.slice(1), id=location.pathname.split('/').pop(), h={'Content-Type':'application/json','X-Carbon-Feedback-Token':token};const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));let rate=0;async function api(method='GET',body){const r=await fetch('/v1/reviews/'+encodeURIComponent(id),{method,headers:h,body:body?JSON.stringify(body):undefined});const d=await r.json();if(!r.ok)throw Error(d.error||'Request failed');return d}function render(review){document.querySelector('#app').innerHTML='<div class="tag">Scoped review</div><h1>'+e(review.title)+'</h1><p>'+e(review.focus)+'</p>'+(review.artifact?'<div class="artifact">'+e(review.artifact)+'</div>':'')+'<p class="muted">Your response is qualitative feedback for this project. Do not include credentials or secrets.</p><form><label>How useful or credible does this feel?</label><div class="rate">'+[1,2,3,4,5].map(n=>'<button type="button" data-rate="'+n+'" aria-pressed="false">'+n+'</button>').join('')+'</div><label>Category</label><select><option>feedback</option><option>bug</option><option>usability</option><option>value</option><option>trust</option><option>idea</option></select><label>What should the team know?</label><textarea required placeholder="What happened, what did you expect, and why does it matter?"></textarea><button class="primary">Send feedback</button><div class="notice"></div></form>';document.querySelectorAll('[data-rate]').forEach(b=>b.onclick=()=>{rate=+b.dataset.rate;document.querySelectorAll('[data-rate]').forEach(x=>x.setAttribute('aria-pressed',+x.dataset.rate===rate))});document.querySelector('form').onsubmit=async ev=>{ev.preventDefault();const notice=document.querySelector('.notice'), response=document.querySelector('textarea').value.trim();if(!rate||!response){notice.textContent='Choose a rating and write feedback first.';return}try{await api('POST',{rating:rate,category:document.querySelector('select').value,response});notice.textContent='Feedback recorded. You can close this page.';ev.target.reset()}catch(err){notice.textContent=err.message}}}api().then(x=>render(x.review)).catch(err=>document.querySelector('#app').textContent=err.message)</script></body></html>`; }

const server = http.createServer(async (req, res) => { try {
  const url = new URL(req.url, publicUrl);
  res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Cache-Control", "no-store");
  if (req.method === "GET" && /^\/review\/[A-Za-z0-9-]+$/.test(url.pathname)) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'" }); res.end(page()); return; }
  if (req.method === "POST" && url.pathname === "/v1/reviews") { if (!dev && !apiKeys.has(String(req.headers["x-carbon-feedback-key"] || ""))) return json(res, 401, { error: "Private service API key required." }); const input=await body(req); const state=load(); const reviewerToken=random(); const review={id:`CFB-${crypto.randomUUID()}`,title:text(input.title,"title",180,true),focus:text(input.focus,"focus",4000,true),artifact:safeArtifact(input.artifact),artifactKind:text(input.artifactKind||"other","artifactKind",40),context:text(input.context,"context",12000),recipient:{email:email(input.recipient?.email),name:text(input.recipient?.name,"recipient.name",180)},status:"queued",createdAt:now(),updatedAt:now(),expiresAt:new Date(Date.now()+48*60*60*1000).toISOString(),reviewerTokenHash:hash(reviewerToken),responses:[],events:[{type:"review.queued",createdAt:now()}]}; state.reviews.push(review); const reviewerUrl=`${publicUrl}/review/${review.id}#${reviewerToken}`; let delivery; try { delivery=await deliverEmail(review,reviewerUrl); } catch(error) { delivery={status:"queued",message:`Invitation queued but mail dispatch failed: ${error.message}`}; } review.delivery=delivery; review.status=delivery.status==="sent"?"sent":"queued"; review.updatedAt=now(); review.events.push({type:`delivery.${delivery.status}`,createdAt:now()}); save(state); return json(res,201,{review:{id:review.id,status:review.status,expiresAt:review.expiresAt},reviewerUrl,delivery}); }
  const ownerMatch=url.pathname.match(/^\/v1\/reviews\/([A-Za-z0-9-]+)\/owner$/); if(ownerMatch){if(!dev&&!apiKeys.has(String(req.headers["x-carbon-feedback-key"]||"")))return json(res,401,{error:"Private service API key required."});const state=load();const review=find(state,ownerMatch[1]);return json(res,200,{review:{id:review.id,status:review.status,updatedAt:review.updatedAt,responses:review.responses.map(({id,rating,category,response,createdAt})=>({id,rating,category,response,createdAt}))}});}
  const match=url.pathname.match(/^\/v1\/reviews\/([A-Za-z0-9-]+)$/); if(match){const state=load();const review=find(state,match[1]);assertReviewer(req,url,review);if(req.method==="GET")return json(res,200,{review:dto(review)});if(req.method==="POST"){const input=await body(req);const rating=Number(input.rating);if(!Number.isInteger(rating)||rating<1||rating>5)throw Object.assign(new Error("rating must be an integer from 1 to 5."),{status:400});const response={id:`FBR-${crypto.randomUUID()}`,rating,category:text(input.category||"feedback","category",80),response:text(input.response,"response",12000,true),createdAt:now()};review.responses.push(response);review.status="feedback_ready";review.updatedAt=now();review.events.push({type:"feedback.recorded",createdAt:now()});save(state);return json(res,201,{ok:true,response:{id:response.id,rating:response.rating,category:response.category,createdAt:response.createdAt}})}return json(res,405,{error:"Method not allowed."});}
  json(res,404,{error:"Not found."});
} catch(error) { json(res,error.status||400,{error:error.message||"Request failed."}); } });
server.listen(port,"0.0.0.0",()=>console.log(`CARBON private Feedback Service listening on ${port}`));
