import { existsSync, readFileSync, statSync } from 'node:fs'
import { basename, resolve } from 'node:path'
import type { Agent } from '@atproto/api'
import { MAX_PRIVATE_SITE_FILE_COUNT, MAX_PRIVATE_SITE_SIZE } from '@wispplace/constants'
import type { OutputSchema as PrivateCreateOutput } from '@wispplace/lexicons/types/place/wisp/v2/privateSite/create'
import type { OutputSchema as PrivateCreateShareOutput } from '@wispplace/lexicons/types/place/wisp/v2/privateSite/createShare'
import type { OutputSchema as PrivateListOutput } from '@wispplace/lexicons/types/place/wisp/v2/privateSite/list'
import type { OutputSchema as PrivateListSharesOutput } from '@wispplace/lexicons/types/place/wisp/v2/privateSite/listShares'
import { lookup } from 'mime-types'
import { authenticateForXrpc, type XrpcCommandOptions } from '../lib/command-utils.ts'
import { createSpinner, pc } from '../lib/progress.ts'
import { parseServiceDid, WISP_PROXY_SERVICE_ID } from '../lib/wisp-service.ts'
import { callWispXrpc, registerWispLexicons } from '../lib/xrpc.ts'
import { collectFiles, createIgnoreMatcher } from './deploy.ts'

export interface PrivateDeployOptions extends XrpcCommandOptions {
	path: string
	name?: string
	expiry?: string
}

export interface PrivateShareOptions extends XrpcCommandOptions {
	label?: string
	expiry?: string
	to?: string
}

const formatBytes = (bytes: number): string => {
	if (bytes < 1024) return `${bytes} B`
	if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
	return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}

const formatExpiry = (expiresAt?: string): string => {
	if (!expiresAt) return pc.dim('never expires')
	const when = new Date(expiresAt)
	const mins = Math.round((when.getTime() - Date.now()) / 60000)
	if (mins <= 0) return pc.red('expired')
	if (mins < 60) return pc.yellow(`expires in ${mins}m`)
	if (mins < 60 * 24) return pc.yellow(`expires in ${Math.round(mins / 60)}h`)
	return pc.dim(`expires ${when.toISOString().slice(0, 10)}`)
}

const parseExpiry = (raw: string | undefined): number | undefined => {
	if (raw === undefined) return undefined
	const value = Number(raw)
	if (!Number.isInteger(value) || value < 0) {
		throw new Error('--expiry must be a non-negative whole number of minutes (0 disables expiry)')
	}
	return value
}
export async function privateDeploy(agent: Agent, options: PrivateDeployOptions): Promise<PrivateCreateOutput> {
	const siteDir = resolve(options.path)
	if (!existsSync(siteDir) || !statSync(siteDir).isDirectory()) {
		throw new Error(`Not a directory: ${siteDir}`)
	}

	const name = (options.name || basename(siteDir)).trim()
	const expiryMinutes = parseExpiry(options.expiry)

	console.log(pc.cyan(`\nCreating private site ${pc.bold(name)} from ${siteDir}\n`))

	const spinner = createSpinner('Scanning directory...').start()
	const ig = createIgnoreMatcher(siteDir)
	const files = collectFiles(siteDir, ig, siteDir)

	if (files.length === 0) {
		spinner.fail('No files to upload')
		throw new Error('No files found to upload')
	}
	if (files.length > MAX_PRIVATE_SITE_FILE_COUNT) {
		spinner.fail('Too many files')
		throw new Error(`Private sites are limited to ${MAX_PRIVATE_SITE_FILE_COUNT} files (found ${files.length})`)
	}

	const totalBytes = files.reduce((sum, f) => sum + f.size, 0)
	if (totalBytes > MAX_PRIVATE_SITE_SIZE) {
		spinner.fail('Site too large')
		throw new Error(
			`Private sites are limited to ${formatBytes(MAX_PRIVATE_SITE_SIZE)} (found ${formatBytes(totalBytes)})`,
		)
	}

	spinner.succeed(`Found ${files.length} files (${formatBytes(totalBytes)})`)

	const form = new FormData()
	form.append('name', name)
	if (expiryMinutes !== undefined) {
		form.append('expiryMinutes', String(expiryMinutes))
	}

	const uploadSpinner = createSpinner('Uploading privately...').start()
	for (const file of files) {
		const bytes = readFileSync(file.path)
		const mime = lookup(file.relativePath) || 'application/octet-stream'
		form.append('files', new File([new Uint8Array(bytes)], file.relativePath, { type: mime }), file.relativePath)
	}
	const serviceDid = parseServiceDid(options.service)
	const proxied = agent.withProxy(WISP_PROXY_SERVICE_ID, serviceDid)
	registerWispLexicons(proxied)
	const response = await proxied.call('place.wisp.v2.privateSite.create', undefined, form)
	const result = response.data as PrivateCreateOutput

	uploadSpinner.succeed('Uploaded')

	console.log()
	console.log(`${pc.bold('Private site:')} ${pc.cyan(result.url)}`)
	console.log(`${pc.dim('site id:')} ${result.siteId}`)
	console.log(`${pc.dim('files:')} ${result.fileCount}  ${pc.dim('size:')} ${formatBytes(result.totalBytes)}`)
	console.log(`${pc.dim('expiry:')} ${formatExpiry(result.expiresAt)}`)
	console.log()
	console.log(pc.dim('Only you can open that URL while signed in.'))
	console.log(pc.dim(`Create a shareable link with: wisp private share ${result.siteId}`))

	return result
}

export async function privateList(agent: Agent, options: XrpcCommandOptions): Promise<void> {
	const spinner = createSpinner('Fetching private sites...').start()
	const data = await callWispXrpc<PrivateListOutput>(agent, 'place.wisp.v2.privateSite.list', {
		serviceDid: options.service,
	})
	spinner.succeed('Fetched private sites')

	if (data.sites.length === 0) {
		console.log(pc.dim('No private sites found.'))
		return
	}

	console.log(pc.bold(`\nPrivate sites (${data.sites.length})`))
	for (const site of data.sites) {
		const header = site.expired ? pc.red(`${site.name} (expired)`) : pc.bold(site.name)
		console.log(`- ${header}`)
		console.log(`  ${pc.dim('id:')} ${site.siteId}`)
		console.log(
			`  ${pc.dim('files:')} ${site.fileCount}  ${pc.dim('size:')} ${formatBytes(site.totalBytes)}  ${formatExpiry(site.expiresAt)}`,
		)
		console.log(`  ${pc.dim('active share links:')} ${site.shareCount}`)
	}
}

export async function privateDelete(agent: Agent, siteId: string, options: XrpcCommandOptions): Promise<void> {
	const spinner = createSpinner('Deleting private site...').start()
	await callWispXrpc(agent, 'place.wisp.v2.privateSite.delete', {
		serviceDid: options.service,
		data: { siteId },
	})
	spinner.succeed(`Deleted private site ${siteId}`)
}

export async function privateShare(agent: Agent, siteId: string, options: PrivateShareOptions): Promise<void> {
	const expiryMinutes = parseExpiry(options.expiry)

	const spinner = createSpinner('Creating share link...').start()
	const data = await callWispXrpc<PrivateCreateShareOutput>(agent, 'place.wisp.v2.privateSite.createShare', {
		serviceDid: options.service,
		data: {
			siteId,
			label: options.label,
			...(expiryMinutes === undefined ? {} : { expiryMinutes }),
			...(options.to ? { audienceDid: options.to } : {}),
		},
	})
	spinner.succeed('Share link created')

	console.log()
	console.log(pc.bold('Shareable link:'))
	console.log(pc.cyan(data.url))
	if (data.directUrl && data.directUrl !== data.url) {
		console.log(pc.dim(`direct: ${data.directUrl}`))
	}
	console.log()
	console.log(`${pc.dim('share id:')} ${data.shareId}  ${formatExpiry(data.expiresAt)}`)
	if (data.audienceDid) {
		console.log(pc.dim(`only ${data.audienceDid} can open this; they will be asked to sign in`))
	}
	console.log(pc.yellow('This link is shown once and cannot be retrieved later. Store it now.'))
	console.log(pc.dim(`Revoke it with: wisp private revoke ${siteId} ${data.shareId}`))
}

export async function privateShares(agent: Agent, siteId: string, options: XrpcCommandOptions): Promise<void> {
	const spinner = createSpinner('Fetching share links...').start()
	const data = await callWispXrpc<PrivateListSharesOutput>(agent, 'place.wisp.v2.privateSite.listShares', {
		serviceDid: options.service,
		params: { siteId },
	})
	spinner.succeed('Fetched share links')

	if (data.shares.length === 0) {
		console.log(pc.dim('No share links for this site.'))
		return
	}

	console.log(pc.bold(`\nShare links (${data.shares.length})`))
	for (const share of data.shares) {
		const color = share.status === 'active' ? pc.green : share.status === 'revoked' ? pc.red : pc.yellow
		const label = share.label ? ` ${pc.dim(`(${share.label})`)}` : ''
		console.log(`- ${share.shareId}${label} ${color(share.status)}`)
		console.log(`  ${pc.dim('token:')} ${share.tokenPrefix}...  ${formatExpiry(share.expiresAt)}`)
		if (share.lastUsedAt) {
			console.log(`  ${pc.dim('last used:')} ${new Date(share.lastUsedAt).toISOString()}`)
		}
	}
}

export async function privateRevoke(
	agent: Agent,
	siteId: string,
	shareId: string,
	options: XrpcCommandOptions,
): Promise<void> {
	const spinner = createSpinner('Revoking share link...').start()
	await callWispXrpc(agent, 'place.wisp.v2.privateSite.revokeShare', {
		serviceDid: options.service,
		data: { siteId, shareId },
	})
	spinner.succeed(`Revoked share ${shareId}`)
}

export { authenticateForXrpc }
