<svelte:head>
	<title>API Key Generator</title>
</svelte:head>

<script lang="js">
	import { copy } from '$lib/clipboard.js';

	const ALPHABETS = {
		hex:       '0123456789abcdef',
		base62:    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
		base58:    '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
		alphanum:  '0123456789abcdefghijklmnopqrstuvwxyz',
		alpha:     'abcdefghijklmnopqrstuvwxyz',
	};

	/**
	 * Generate cryptographically secure random string from alphabet.
	 * Uses rejection sampling to avoid modulo bias.
	 * @param {string} alphabet
	 * @param {number} length
	 * @returns {string}
	 */
	function randomString(alphabet, length) {
		const chars = [];
		// Find largest multiple of alphabet.length that fits in a byte (rejection sampling)
		const max = 256 - (256 % alphabet.length);
		while (chars.length < length) {
			const bytes = crypto.getRandomValues(new Uint8Array(length * 2));
			for (const b of bytes) {
				if (b < max) {
					chars.push(alphabet[b % alphabet.length]);
					if (chars.length === length) break;
				}
			}
		}
		return chars.join('');
	}

	/**
	 * Generate raw bytes as base64url (no padding).
	 * @param {number} byteCount
	 * @returns {string}
	 */
	function randomBase64url(byteCount) {
		const bytes = crypto.getRandomValues(new Uint8Array(byteCount));
		const base64 = btoa(String.fromCharCode(...bytes));
		return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
	}

	/**
	 * @param {number} bits
	 * @returns {string}
	 */
	function randomHex(bits) {
		const bytes = crypto.getRandomValues(new Uint8Array(bits / 8));
		return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
	}

	/**
	 * @typedef {{ id: string, label: string, desc: string }} Format
	 */

	/** @type {Format[]} */
	const formats = [
		{ id: 'hex128',     label: 'hex-128',      desc: '128-bit hex (32 chars)' },
		{ id: 'hex256',     label: 'hex-256',      desc: '256-bit hex (64 chars)' },
		{ id: 'base64url',  label: 'base64url',    desc: '256-bit base64url (43 chars)' },
		{ id: 'base62_32',  label: 'base62-32',    desc: '32 chars base62 (~190 bits)' },
		{ id: 'base62_43',  label: 'base62-43',    desc: '43 chars base62 (~256 bits)' },
		{ id: 'base58_32',  label: 'base58-32',    desc: '32 chars base58 (~187 bits)' },
		{ id: 'alphanum_32',label: 'alnum-32',     desc: '32 lowercase alphanumeric (~165 bits)' },
		{ id: 'custom',     label: 'custom',       desc: 'custom alphabet & length' },
	];

	/** @param {string} id @param {string} alphabet @param {number} customLen @returns {string} */
	function generate(id, alphabet, customLen) {
		switch (id) {
			case 'hex128':      return randomHex(128);
			case 'hex256':      return randomHex(256);
			case 'base64url':   return randomBase64url(32);
			case 'base62_32':   return randomString(ALPHABETS.base62, 32);
			case 'base62_43':   return randomString(ALPHABETS.base62, 43);
			case 'base58_32':   return randomString(ALPHABETS.base58, 32);
			case 'alphanum_32': return randomString(ALPHABETS.alphanum, 32);
			case 'custom':      return randomString(alphabet || ALPHABETS.base62, customLen);
			default:            return '';
		}
	}

	/** @param {string} alphabet @param {number} length @returns {number} */
	const entropy = (alphabet, length) => Math.floor(Math.log2(alphabet.length) * length);

	/** @param {string} id @param {string} customAlphabet @param {number} customLen @returns {number} */
	function entropyFor(id, customAlphabet, customLen) {
		switch (id) {
			case 'hex128':      return 128;
			case 'hex256':      return 256;
			case 'base64url':   return 256;
			case 'base62_32':   return entropy(ALPHABETS.base62, 32);
			case 'base62_43':   return entropy(ALPHABETS.base62, 43);
			case 'base58_32':   return entropy(ALPHABETS.base58, 32);
			case 'alphanum_32': return entropy(ALPHABETS.alphanum, 32);
			case 'custom':      return entropy(customAlphabet || ALPHABETS.base62, customLen);
			default:            return 0;
		}
	}

	// --- state ---

	let selectedFormat = $state('hex256');
	let prefix         = $state('');
	let customAlphabet = $state(ALPHABETS.base62);
	let customLength   = $state(32);
	let batchCount     = $state(5);

	/** @type {{ key: string }[]} */
	let keys = $state([]);

	function regenerate() {
		keys = Array.from({ length: batchCount }, () => ({
			key: (prefix ? prefix + '_' : '') + generate(selectedFormat, customAlphabet, customLength),
		}));
	}

	// generate on load
	regenerate();

	const currentEntropy = $derived(entropyFor(selectedFormat, customAlphabet, customLength));
</script>

<h1>API Key Generator</h1>

<h2>Format</h2>
<table class="formats">
	<tbody>
		{#each formats as f}
			<tr>
				<td>
					<label>
						<input type="radio" bind:group={selectedFormat} value={f.id} onchange={regenerate} />
						{f.label}
					</label>
				</td>
				<td class="desc">{f.desc}</td>
			</tr>
		{/each}
	</tbody>
</table>

{#if selectedFormat === 'custom'}
<div class="custom-opts">
	<label>
		Alphabet:
		<input type="text" bind:value={customAlphabet} size="50" spellcheck="false" oninput={regenerate} />
		<span class="dim">({customAlphabet.length} chars)</span>
	</label>
	<br>
	<label>
		Length:
		<input type="number" bind:value={customLength} min="8" max="256" size="5" oninput={regenerate} />
	</label>
	<br>
	<div class="quick-alpha">
		{#each Object.entries(ALPHABETS) as [name, abc]}
			<button onclick={() => { customAlphabet = abc; regenerate(); }}>{name}</button>
		{/each}
	</div>
</div>
{/if}

<h2>Options</h2>
<p>
	<label>
		Prefix (optional):
		<input type="text" bind:value={prefix} size="16" placeholder="sk, pk, token…" spellcheck="false" oninput={regenerate} />
	</label>
	&nbsp;
	<label>
		Count:
		<input type="number" bind:value={batchCount} min="1" max="50" size="4" oninput={regenerate} />
	</label>
</p>

<h2>Keys <span class="entropy">~{currentEntropy} bits of entropy</span></h2>
<div class="actions">
	<button onclick={regenerate}>Regenerate all</button>
	<button onclick={() => copy(keys.map(k => k.key).join('\n'))}>Copy all</button>
</div>
<table class="keys">
	<tbody>
		{#each keys as k, i}
			<tr>
				<td class="idx">{i + 1}</td>
				<td><code>{k.key}</code></td>
				<td>
					<button onclick={() => {
						k.key = (prefix ? prefix + '_' : '') + generate(selectedFormat, customAlphabet, customLength);
					}}>new</button>
				</td>
				<td><button onclick={() => copy(k.key)}>copy</button></td>
			</tr>
		{/each}
	</tbody>
</table>

<style>
	.formats {
		border-collapse: collapse;
		margin-bottom: 8px;
	}

	.formats td {
		padding: 2px 10px 2px 0;
	}

	.desc {
		color: var(--fg-dim);
		font-size: 0.9em;
	}

	.custom-opts {
		border: 1px solid var(--border);
		padding: 8px 12px;
		margin-bottom: 8px;
		background: var(--bg-panel);
	}

	.custom-opts label {
		display: block;
		margin-bottom: 4px;
	}

	.quick-alpha {
		display: flex;
		gap: 4px;
		flex-wrap: wrap;
		margin-top: 6px;
	}

	.dim {
		color: var(--fg-faint);
		font-size: 0.85em;
	}

	.entropy {
		font-size: 0.75em;
		font-weight: normal;
		color: var(--green);
		margin-left: 8px;
	}

	.actions {
		display: flex;
		gap: 6px;
		margin-bottom: 6px;
	}

	.keys {
		border-collapse: collapse;
		width: 100%;
	}

	.keys td {
		padding: 2px 6px;
		border: 1px solid var(--border-faint);
	}

	.keys code {
		font-size: 0.95em;
		word-break: break-all;
	}

	.idx {
		color: var(--fg-faint);
		font-size: 0.85em;
		width: 1px;
		white-space: nowrap;
		text-align: right;
	}

	.keys td:nth-child(3),
	.keys td:nth-child(4) {
		width: 1px;
		white-space: nowrap;
	}
</style>
