/*
  This file is part of Paivana.
  Copyright (C) 2026 Taler Systems SA

  Paivana is free software; you can redistribute it and/or
  modify it under the terms of the GNU Affero General Public License
  as published by the Free Software Foundation; either version
  3, or (at your option) any later version.

  Paivana is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public
  License along with Paivana; see the file COPYING.  If not,
  write to the Free Software Foundation, Inc., 51 Franklin
  Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// merchant_stub: the two GET endpoints of the GNU Taler merchant
// backend that paivana reads at startup, and nothing else.
//
// `benchmark.sh -m paywall' needs paivana running with the paywall
// *on*, and paivana does not open its listen socket until it has
// fetched a template: PAIVANA_HTTPD_load_templates() ->
// check_templates() -> setup_template() -> templates_ready() ->
// PAIVANA_HTTPD_serve_requests().  So something has to answer those
// two requests before anything can be measured at all.
//
// Nothing after that exchange touches the backend again.  The paywall
// page is rendered locally from the template, and the rendered
// MHD_Response is then cached per (language, encoding) in
// load_paywall(), so across the measurement window this process is
// idle and would be equally idle if it were a real merchant.  That is
// what makes a stub honest here where test_paywall.sh needs the real
// thing: this benchmark never buys anything, so no code path that
// distinguishes the two is ever reached.  `benchmark.sh' takes
// PAIVANA_BENCH_MERCHANT_URL for anyone who wants to check that claim
// against a live backend.
//
// The two bodies are shaped as the merchant client library parses
// them -- merchant_api_get-private-templates.c wants `templates' with
// `template_id' and `template_description', and
// merchant_api_get-private-templates-TEMPLATE_ID.c wants
// `template_description' and an object `template_contract' -- and the
// contract itself is the one test_paywall.sh POSTs to a real backend,
// so the page paivana renders here is the page it renders there.

use std::env;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

// The template paivana is expected to find.  Fixed rather than
// configurable: benchmark.sh has to name it in the URL it measures.
const TEMPLATE_ID: &str = "premium";

fn body_templates() -> String {
    format!(
        "{{\"templates\":[{{\"template_id\":\"{}\",\
         \"template_description\":\"Paywalled content\"}}]}}",
        TEMPLATE_ID
    )
}

fn body_template() -> String {
    // website_regex `.*' and a single choice, i.e. what
    // test_paywall.sh creates.  One choice rather than several because
    // that is the shipped shape: with two or more the page grows a
    // selector (`has_choices'), which would make the measured page
    // depend on a decision this stub had made.
    String::from(
        "{\"template_description\":\"Paywalled content\",\
         \"template_contract\":{\"template_type\":\"paivana\",\
         \"summary\":\"Access to the article\",\
         \"website_regex\":\".*\",\
         \"max_pickup_duration\":{\"d_us\":3600000000},\
         \"choices\":[{\"amount\":\"TESTKUDOS:1\",\
         \"description\":\"One article\"}]}}",
    )
}

fn send(stream: &mut TcpStream, code: u16, reason: &str, body: &str) {
    // `Connection: close' for the same reason upstream_rs sends it
    // (see its client_loop()): this server answers one request per
    // connection and then drops the stream, and paivana's merchant
    // handles sit on a shared curl multi handle that would otherwise
    // reuse a connection we have already shut.
    let head = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\n\
         Content-Length: {}\r\nConnection: close\r\n\r\n",
        code,
        reason,
        body.len()
    );
    let _ = stream.write_all(head.as_bytes());
    let _ = stream.write_all(body.as_bytes());
}

fn client_loop(mut stream: TcpStream, token: &str) {
    let (method, path, authorized) = {
        let mut br = BufReader::new(&mut stream);
        let mut line = String::new();
        if br.read_line(&mut line).unwrap_or(0) == 0 {
            return;
        }
        let mut parts = line.trim_end().splitn(3, ' ');
        let method = match parts.next() {
            Some(m) => m.to_string(),
            None => return,
        };
        let path = match parts.next() {
            Some(p) => p.to_string(),
            None => return,
        };
        let mut authorized = false;
        loop {
            let mut h = String::new();
            if br.read_line(&mut h).unwrap_or(0) == 0 {
                break;
            }
            let t = h.trim_end();
            if t.is_empty() {
                break;
            }
            if let Some(idx) = t.find(':') {
                if t[..idx].trim().eq_ignore_ascii_case("Authorization")
                    && t[idx + 1..].trim() == format!("Bearer {}", token)
                {
                    authorized = true;
                }
            }
        }
        (method, path, authorized)
    };

    eprintln!("merchant_stub: {} {}", method, path);
    // Checked rather than ignored: paivana building the Authorization
    // header out of MERCHANT_ACCESS_TOKEN is the one thing about this
    // exchange that could silently regress, and a 401 here makes
    // paivana say so ("Access to templates unauthorized") instead of
    // starting anyway and leaving the benchmark to measure a
    // configuration nobody would deploy.
    if !authorized {
        send(&mut stream, 401, "Unauthorized",
             "{\"code\":2000,\"hint\":\"merchant_stub: no or wrong bearer token\"}");
        return;
    }
    if method != "GET" {
        send(&mut stream, 405, "Method Not Allowed", "{}");
        return;
    }
    // The path arrives with whatever prefix MERCHANT_BACKEND_URL had;
    // benchmark.sh points it at the root, so these are exact.
    if path == "/private/templates" {
        send(&mut stream, 200, "OK", &body_templates());
        return;
    }
    if path == format!("/private/templates/{}", TEMPLATE_ID) {
        send(&mut stream, 200, "OK", &body_template());
        return;
    }
    send(&mut stream, 404, "Not Found",
         "{\"code\":2906,\"hint\":\"merchant_stub: no such endpoint\"}");
}

fn main() {
    // Same reasoning as upstream_rs: a port that does not parse must
    // be an error rather than a silent fallback, or the caller waits
    // out its readiness timeout on a port nothing ever bound.
    let port: u16 = match env::args().nth(1) {
        None => 8405,
        Some(s) => match s.parse::<u16>() {
            Ok(p) if p >= 1 => p,
            _ => {
                eprintln!("invalid port {:?}", s);
                std::process::exit(1);
            }
        },
    };
    let token = env::args().nth(2).unwrap_or_else(|| String::from("secret-token:stub"));
    // Loopback only: it hands out a merchant configuration to anyone
    // who asks with the right token, and has no business being
    // reachable from the network.
    let listener = TcpListener::bind(("127.0.0.1", port)).expect("bind failed");
    eprintln!("merchant_stub listening on port {}", port);
    for stream in listener.incoming() {
        match stream {
            Ok(s) => {
                let token = token.clone();
                thread::spawn(move || client_loop(s, &token));
            }
            Err(_) => continue,
        }
    }
}
