/**
 * Account storage on the shared `recflare` D1 database. Each account is a single
 * JSON blob in the `data` column; queryable fields (AccountId, Username) are
 * SQLite generated (virtual) columns extracted from that JSON and indexed —
 * the same JSON-blob pattern the `rooms` worker uses.
 *
 * The `auth` worker owns this schema/migration (see apps/auth/migrations/
 * 0001_accounts.sql, applied with its own `migrations_table` so it doesn't clash
 * with the rooms migrations that share the database). This module is the single
 * source of truth for the helpers; the `auth` and `accounts` workers both import
 * it from `@repo/domain` (each uses the subset it needs).
 */

/** Schema DDL (mirror of migrations 0001_accounts + 0002_avatar, sans seed INSERTs). */
export const SCHEMA_DDL: string[] = [
	`CREATE TABLE IF NOT EXISTS accounts (
		data TEXT NOT NULL,
		avatar TEXT,
		account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
		username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL
	)`,
	`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON accounts (account_id)`,
	`CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON accounts (username_lower)`,
]

/** Client-facing account shape (camelCase, exactly as the client's AccountDTO). */
export interface Account {
	accountId: number
	username: string
	displayName: string
	profileImage: string
	isJunior: boolean
	platforms: number
	personalPronouns: number
	identityFlags: number
	createdAt: string
	/** Set via POST /account/me/email; absent until the player provides one. */
	email?: string
	/** Set via POST /account/me/phone; absent until the player provides one. */
	phone?: string
	/** Set via PUT /account/me/bio; read back via GET /account/:id/bio. */
	bio?: string
	/** Remaining username changes; decremented by PUT /account/me/username. */
	availableUsernameChanges?: number
	/**
	 * PBKDF2 `salt:hash` for credential login; set via /account/me/changepassword
	 * or create_account. Kept in the JSON blob but never projected into a public
	 * DTO (the DTO builders pick only known fields), so it doesn't leak.
	 */
	passwordHash?: string
}

interface AccountRow {
	data: string
}

const parseOne = (row: AccountRow | null): Account | null =>
	row ? (JSON.parse(row.data) as Account) : null
const parseAll = (rows: AccountRow[]): Account[] => rows.map((r) => JSON.parse(r.data) as Account)

/** Word lists for auto-assigned usernames (players don't pick one on signup). */
const ADJECTIVES = [
	'Swift',
	'Brave',
	'Clever',
	'Happy',
	'Mighty',
	'Lucky',
	'Sunny',
	'Cosmic',
	'Witty',
	'Nimble',
	'Jolly',
	'Bold',
	'Gentle',
	'Fuzzy',
	'Speedy',
	'Shiny',
]
const NOUNS = [
	'Fox',
	'Otter',
	'Falcon',
	'Panda',
	'Tiger',
	'Comet',
	'Maple',
	'Pixel',
	'Robin',
	'Wolf',
	'Koala',
	'Dragon',
	'Penguin',
	'Badger',
	'Heron',
	'Lynx',
]

/** A random, readable username (e.g. "SwiftFox4821"). */
export function randomUsername(): string {
	const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
	const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
	const n = Math.floor(Math.random() * 10000)
	return `${adj}${noun}${n}`
}

/**
 * Build a full account object from an id, applying default fallbacks for any
 * column the caller doesn't override. Used both to synthesize accounts that
 * aren't in the DB and as the base for a freshly created account.
 */
export function defaultAccount(id: number, overrides: Partial<Account> = {}): Account {
	return {
		accountId: id,
		username: `Player${id}`,
		displayName: `Player${id}`,
		profileImage: 'DefaultProfileImage.jpg',
		isJunior: false,
		platforms: 0,
		personalPronouns: 0,
		identityFlags: 0,
		createdAt: new Date().toISOString(),
		...overrides,
	}
}

/** Look up a single account by AccountId. */
export async function getAccount(db: D1Database, id: number): Promise<Account | null> {
	return parseOne(
		await db.prepare('SELECT data FROM accounts WHERE account_id = ?1').bind(id).first<AccountRow>()
	)
}

/** Look up a single account by username (case-insensitive), or null if none. */
export async function getAccountByUsername(
	db: D1Database,
	username: string
): Promise<Account | null> {
	return parseOne(
		await db
			.prepare('SELECT data FROM accounts WHERE username_lower = ?1')
			.bind(username.toLowerCase())
			.first<AccountRow>()
	)
}

/** Default cap on how many matches `searchAccounts` returns. */
export const SEARCH_LIMIT = 20

/** Escape LIKE wildcards so user input is matched literally (using `\` as the escape char). */
const escapeLike = (s: string): string => s.replace(/[\\%_]/g, '\\$&')

/**
 * Prefix-search accounts by username (case-insensitive, "begins with"), ordered
 * alphabetically. Backed by the indexed `username_lower` generated column, so the
 * `name%` LIKE stays index-friendly. Returns up to `limit` matches.
 */
export async function searchAccounts(
	db: D1Database,
	name: string,
	limit = SEARCH_LIMIT
): Promise<Account[]> {
	const q = name.trim().toLowerCase()
	if (q === '') return []
	const { results } = await db
		.prepare(
			`SELECT data FROM accounts WHERE username_lower LIKE ?1 ESCAPE '\\' ORDER BY username_lower LIMIT ?2`
		)
		.bind(`${escapeLike(q)}%`, limit)
		.all<AccountRow>()
	return parseAll(results)
}

/** Look up multiple accounts by AccountId (order not guaranteed). */
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
	if (ids.length === 0) return []
	const placeholders = ids.map((_, i) => `?${i + 1}`).join(',')
	const { results } = await db
		.prepare(`SELECT data FROM accounts WHERE account_id IN (${placeholders})`)
		.bind(...ids)
		.all<AccountRow>()
	return parseAll(results)
}

/**
 * Merge `overrides` into the account row for `id` and persist it. Reads the
 * current account (falling back to a synthesized default), applies the
 * overrides, and writes the whole JSON blob back — inserting the row when the
 * account isn't in the table yet. Returns the updated account.
 */
export async function updateAccount(
	db: D1Database,
	id: number,
	overrides: Partial<Account>
): Promise<Account> {
	const current = (await getAccount(db, id)) ?? defaultAccount(id)
	const updated: Account = { ...current, ...overrides, accountId: id }
	const data = JSON.stringify(updated)
	const res = await db
		.prepare('UPDATE accounts SET data = ?2 WHERE account_id = ?1')
		.bind(id, data)
		.run()
	if (!res.meta.changes) {
		await db.prepare('INSERT INTO accounts (data) VALUES (?1)').bind(data).run()
	}
	return updated
}

/**
 * Create and persist a new account. The id is the next free integer (above the
 * seeded system accounts); the username is auto-assigned (players don't choose
 * one initially) and the display name defaults to it.
 */
export async function createAccount(
	db: D1Database,
	overrides: Partial<Account> = {}
): Promise<Account> {
	const row = await db
		.prepare('SELECT COALESCE(MAX(account_id), 1) + 1 AS next FROM accounts')
		.first<{ next: number }>()
	const id = row?.next ?? 2
	const username = overrides.username ?? randomUsername()
	const account = defaultAccount(id, { username, displayName: username, ...overrides })
	await db.prepare('INSERT INTO accounts (data) VALUES (?1)').bind(JSON.stringify(account)).run()
	return account
}

/**
 * Read the account's stored password hash (`salt:hash`), or null when the account
 * has none / doesn't exist. Kept in the account JSON blob but out of the public
 * account DTO (which projects only known fields), so it never leaks.
 */
export async function getPasswordHash(db: D1Database, id: number): Promise<string | null> {
	const row = await db
		.prepare(
			"SELECT json_extract(data, '$.passwordHash') AS hash FROM accounts WHERE account_id = ?1"
		)
		.bind(id)
		.first<{ hash: string | null }>()
	return row?.hash ?? null
}

/** Persist the account's password hash. Returns false when no such account exists. */
export async function setPasswordHash(db: D1Database, id: number, hash: string): Promise<boolean> {
	const { meta } = await db
		.prepare(
			"UPDATE accounts SET data = json_set(data, '$.passwordHash', ?2) WHERE account_id = ?1"
		)
		.bind(id, hash)
		.run()
	return meta.changes > 0
}
