// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"html"
	"log"
	"text/template"
)

var tmpl *template.Template

func init() {
	var err error
	tmpl, err = template.New("powxy").Parse(`<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<title>Proof of Work Challenge</title>
	<style>
		html {
			font-family: sans-serif;
			background-color: var(--background-color);
			color: var(--text-color);
			--radius-1: 0.32rem;
			--background-color: hsl(0, 0%, 100%);
			--text-color: hsl(0, 0%, 0%);
			--link-color: hsl(320, 50%, 36%);
			--light-text-color: hsl(0, 0%, 45%);
			--darker-border-color: hsl(0, 0%, 72%);
			--lighter-border-color: hsl(0, 0%, 85%);
			--text-decoration-color: hsl(0, 0%, 72%);
			--darker-box-background-color: hsl(0, 0%, 92%);
			--lighter-box-background-color: hsl(0, 0%, 95%);
			--primary-color: hsl(320, 50%, 36%);
			--primary-color-contrast: hsl(320, 0%, 100%);
			--danger-color: #ff0000;
			--danger-color-contrast: #ffffff;
		}

		@media (prefers-color-scheme: dark) {
			html {
				--background-color: hsl(0, 0%, 0%);
				--text-color: hsl(0, 0%, 100%);
				--link-color: hsl(320, 50%, 76%);
				--light-text-color: hsl(0, 0%, 78%);
				--darker-border-color: hsl(0, 0%, 35%);
				--lighter-border-color: hsl(0, 0%, 25%);
				--text-decoration-color: hsl(0, 0%, 30%);
				--darker-box-background-color: hsl(0, 0%, 20%);
				--lighter-box-background-color: hsl(0, 0%, 15%);
			}
		}

		body {
			margin: 0;
			padding: 1rem;
		}

		main {
			max-width: 720px;
			margin: 0 auto;
		}

		*:focus-visible {
			outline: 1.5px var(--primary-color) solid;
		}

		section {
			margin: 0;
		}

		label {
			display: block;
			font-style: italic;
			margin-top: 1rem;
			margin-bottom: 0.5rem;
		}

		h1 {
			margin-top: 0;
			color: var(--primary-color);
		}

		p, summary {
			line-height: 1.2;
		}

		a {
			color: var(--link-color);
			text-decoration-color: var(--text-decoration-color);
		}

		input[type="text"] {
			background-color: var(--lighter-box-background-color);
			width: 100%;
			padding: 0.5rem;
			border-radius: var(--radius-1);
			border: none;
			box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.15);
			margin-bottom: 1rem;
			box-sizing: border-box;
		}

		input[type="submit"] {
			padding: 0.5rem 1rem;
			background-color: var(--primary-color);
			color: var(--primary-color-contrast);
			border: none;
			border-radius: var(--radius-1);
			cursor: pointer;
		}

		input[readonly] {
			background-color: var(--lighter-box-background-color);
			color: var(--text-color);
			cursor: text;
		}

		details {
			margin-top: 2rem;
			background-color: var(--lighter-box-background-color);
			padding: 0.5rem;
			border-radius: var(--radius-1);
			box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.15);
		}

		pre {
			overflow-x: auto;
			white-space: pre-wrap;
			word-break: break-word;
		}

		#solver_status {
			font-size: 0.9rem;
			color: var(--light-text-color);
			margin-top: 1rem;
		}
	</style>
	<script>
		/*    
		@licstart  The following is the entire license notice for the 
		JavaScript code in this page.
		
		Copyright (C) 2025  Runxi Yu
		
		The JavaScript code in this page is free software: you can
		redistribute it and/or modify it under the terms of the GNU
		General Public License (GNU GPL) as published by the Free Software
		Foundation, either version 3 of the License, or (at your option)
		any later version.  The code is distributed WITHOUT ANY WARRANTY;
		without even the implied warranty of MERCHANTABILITY or FITNESS
		FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
		
		As additional permission under GNU GPL version 3 section 7, you
		may distribute non-source (e.g., minimized or compacted) forms of
		that code without the copy of the GNU GPL normally required by
		section 4, provided you include this license notice and a URL
		through which recipients can access the Corresponding Source.   
		
		@licend  The above is the entire license notice
		for the JavaScript code in this page.
		*/
	</script>
</head>
<body>
	<main>
		<header>
			<h1>Proof of Work Challenge</h1>
		</header>

		<section>
			<p>This site is protected by Powxy{{ if .Global.Version }}, version {{ .Global.Version }}{{ end }}.</p>
			<p>You must complete this proof of work challenge before you can access this site.</p>
		</section>

		{{- if .Message }}
		<section>
			<p><strong>{{ .Message }}</strong></p>
		</section>
		{{- end }}

		<section>
			<p>Select a nonce no longer than 32 bytes, such that when it is appended to the decoded form of the challenge token, and the entire result is hashed with SHA-256, the first {{ .Global.NeedBits }} bits of the SHA-256 hash are all zeros. Within one octet, higher bits are considered to come before lower bits.</p>
			<label for="unsigned-token">Challenge token (read-only)</label>
			<input id="unsigned-token" type="text" readonly disabled tabindex="-1" value="{{ .UnsignedTokenBase64 }}" />
		</section>

		<section>
			<form method="POST">
				<p>Encode your selected nonce in base64 and submit it below.</p>
				<p>Please note that if your submission is successful, you will be given a cookie that will allow you to access this site for a period of time without having to complete the challenge again. By pressing the submit button, you agree to be given cookies for this purpose.</p>
				<label id="nonce" for="unsigned-token">Nonce</label>
				<input id="nonce" name="powxy" type="text" />
				<input type="submit" value="Submit" />
			</form>
		</section>

		<section>
			<p id="solver_status">JavaScript seems to be disabled. You must solve the challenge externally.</p>
		</section>

		<details>
			<summary>Offline solver program</summary>
			<pre>` + html.EscapeString(solverProgram) + `</pre>
		</details>

		<p>
			Powxy is free software: you can redistribute it and/or modify it under the terms of the
			<a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License, version 3</a>,
			as published by the Free Software Foundation. Powxy 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 may wish to view the <a href="{{ .Global.SourceURL }}">source code</a>.
		</p>
	</main>

	<script>
		document.addEventListener("DOMContentLoaded", function() {
			let challenge_b64 = "{{ .UnsignedTokenBase64 }}";
			let difficulty = {{ .Global.NeedBits }};
			let form = document.querySelector("form");
			let field = form.querySelector("input[name='powxy']");
			let status_el = document.getElementById("solver_status");
	
			if (!window.crypto || !window.crypto.subtle) {
				status_el.textContent = "SubtleCrypto not available. You must solve the challenge externally.";
				return;
			}
			status_el.textContent = "SubtleCrypto detected. Attempting to solve the challenge automatically...";
	
			let solver_active = true;
			form.addEventListener("submit", function() {
				solver_active = false;
			});
	
			async function solve_pow() {
				let token_bytes = Uint8Array.from(
					atob(challenge_b64),
					ch => ch.charCodeAt(0)
				);
	
				let nonce = 0n;
				let buf = new ArrayBuffer(8);
				let view = new DataView(buf);
	
				while (solver_active) {
					view.setBigUint64(0, nonce, true);
	
					let candidate = new Uint8Array(token_bytes.length + 8);
					candidate.set(token_bytes, 0);
					candidate.set(new Uint8Array(buf), token_bytes.length);
	
					let digest_buffer = await crypto.subtle.digest("SHA-256", candidate);
					let digest = new Uint8Array(digest_buffer);
	
					if (has_leading_zero_bits(digest, difficulty)) {
						let nonce_str = String.fromCharCode(...new Uint8Array(buf));
						field.value = btoa(nonce_str);
	
						status_el.textContent = "A solution has been found automatically in " + nonce + " iterations.";
						return;
					}
	
					nonce++;
	
					if ((nonce & 0x00FFn) === 0n) {
						status_el.textContent = "Attempting to solve automatically. Tried " + nonce + " candidates so far...";
						await new Promise(r => setTimeout(r, 0));
					}
				}
			}
	
			function has_leading_zero_bits(digest, bits) {
				let full_bytes = bits >>> 3;
				for (let i = 0; i < full_bytes; i++) {
					if (digest[i] !== 0) {
						return false;
					}
				}
				let remainder = bits & 7;
				if (remainder !== 0) {
					let mask = 0xFF << (8 - remainder);
					if ((digest[full_bytes] & mask) !== 0) {
						return false;
					}
				}
				return true;
			}
	
			solve_pow();
		});
	</script>
</body>
</html>`)
	if err != nil {
		log.Fatal(err)
	}
}