/**
 * rTime bridge — push/pull time entries between backlog-md and rTime.
 *
 * Offline-first: entries queue in .backlog/rtime-queue.json when rTime
 * is unreachable, and replay on `backlog rtime sync`.
 */

import { join } from "node:path";
import type { BacklogConfig, Task, TimeEntry } from "../types/index.ts";

// ── Config types ──

export interface RtimeConfig {
	baseUrl?: string;
	space?: string;
	memberName?: string;
	token?: string;
	defaultSkill?: string;
	autoPush?: boolean;
	skillMap?: Record<string, string>;
}

export interface QueueEntry {
	taskId: string;
	taskTitle: string;
	entry: TimeEntry;
	queuedAt: string;
}

// ── Skill resolution ──

const SKILL_ALIASES: Record<string, string> = {
	dev: "tech",
	development: "tech",
	engineering: "tech",
	code: "tech",
	coding: "tech",
	programming: "tech",
	ux: "design",
	ui: "design",
	graphics: "design",
	art: "design",
	comms: "outreach",
	marketing: "outreach",
	community: "outreach",
	ops: "logistics",
	admin: "logistics",
	coordination: "logistics",
	planning: "logistics",
	meeting: "facilitation",
	workshop: "facilitation",
	hosting: "facilitation",
};

const VALID_SKILLS = ["facilitation", "design", "tech", "outreach", "logistics"];

/**
 * 3-tier skill resolution: exact match → config map → alias table.
 */
export function resolveSkill(skill: string | undefined, skillMap?: Record<string, string>): string | undefined {
	if (!skill) return undefined;
	const lower = skill.toLowerCase().trim();

	// Tier 1: exact match
	if (VALID_SKILLS.includes(lower)) return lower;

	// Tier 2: config skill map
	if (skillMap) {
		const mapped = skillMap[lower] ?? skillMap[skill];
		if (mapped && VALID_SKILLS.includes(mapped.toLowerCase())) return mapped.toLowerCase();
	}

	// Tier 3: built-in alias table
	const alias = SKILL_ALIASES[lower];
	if (alias) return alias;

	// Fallback: return as-is (rTime will validate)
	return lower;
}

// ── Queue management ──

function queuePath(cwd: string): string {
	return join(cwd, ".backlog", "rtime-queue.json");
}

export async function loadQueue(cwd: string): Promise<QueueEntry[]> {
	try {
		const file = Bun.file(queuePath(cwd));
		if (!(await file.exists())) return [];
		return await file.json();
	} catch {
		return [];
	}
}

async function saveQueue(cwd: string, queue: QueueEntry[]): Promise<void> {
	const path = queuePath(cwd);
	const dir = join(cwd, ".backlog");
	const { mkdirSync, existsSync } = await import("node:fs");
	if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
	await Bun.write(path, JSON.stringify(queue, null, "\t"));
}

async function enqueue(cwd: string, entry: QueueEntry): Promise<void> {
	const queue = await loadQueue(cwd);
	queue.push(entry);
	await saveQueue(cwd, queue);
}

// ── API helpers ──

function getAuthHeaders(config: RtimeConfig): Record<string, string> {
	const token = config.token || process.env.RSPACE_TOKEN;
	const headers: Record<string, string> = { "Content-Type": "application/json" };
	if (token) headers.Authorization = `Bearer ${token}`;
	return headers;
}

function getApiUrl(config: RtimeConfig, path: string): string {
	const base = config.baseUrl || "https://rspace.online";
	const space = config.space || "demo";
	return `${base}/${space}/rtime${path}`;
}

// ── Push ──

export async function pushTimeEntry(
	task: Task,
	entry: TimeEntry,
	cwd: string,
	config: RtimeConfig,
): Promise<{ success: boolean; commitmentId?: string; error?: string }> {
	const url = getApiUrl(config, "/api/external-time-logs");
	const skill = resolveSkill(entry.skill, config.skillMap) || config.defaultSkill || "tech";

	const body = {
		backlogTaskId: task.id,
		backlogTaskTitle: task.title,
		memberName: config.memberName || "Unknown",
		hours: entry.hours,
		skill,
		note: entry.note,
		loggedAt: new Date(entry.date).getTime(),
	};

	try {
		const resp = await fetch(url, {
			method: "POST",
			headers: getAuthHeaders(config),
			body: JSON.stringify(body),
			signal: AbortSignal.timeout(10_000),
		});

		if (!resp.ok) {
			const errBody = await resp.text();
			throw new Error(`HTTP ${resp.status}: ${errBody}`);
		}

		const result = (await resp.json()) as { log: { id: string }; commitmentId: string };
		return { success: true, commitmentId: result.commitmentId };
	} catch (err) {
		const error = err instanceof Error ? err.message : String(err);

		// Queue for offline retry
		await enqueue(cwd, {
			taskId: task.id,
			taskTitle: task.title,
			entry,
			queuedAt: new Date().toISOString(),
		});

		return { success: false, error };
	}
}

// ── Flush queue ──

export async function flushQueue(
	cwd: string,
	config: RtimeConfig,
): Promise<{ flushed: number; failed: number; remaining: number }> {
	const queue = await loadQueue(cwd);
	if (queue.length === 0) return { flushed: 0, failed: 0, remaining: 0 };

	const remaining: QueueEntry[] = [];
	let flushed = 0;
	let failed = 0;

	for (const item of queue) {
		const url = getApiUrl(config, "/api/external-time-logs");
		const skill = resolveSkill(item.entry.skill, config.skillMap) || config.defaultSkill || "tech";

		try {
			const resp = await fetch(url, {
				method: "POST",
				headers: getAuthHeaders(config),
				body: JSON.stringify({
					backlogTaskId: item.taskId,
					backlogTaskTitle: item.taskTitle,
					memberName: config.memberName || "Unknown",
					hours: item.entry.hours,
					skill,
					note: item.entry.note,
					loggedAt: new Date(item.entry.date).getTime(),
				}),
				signal: AbortSignal.timeout(10_000),
			});

			if (resp.ok) {
				flushed++;
			} else {
				remaining.push(item);
				failed++;
			}
		} catch {
			remaining.push(item);
			failed++;
		}
	}

	await saveQueue(cwd, remaining);
	return { flushed, failed, remaining: remaining.length };
}

// ── Pull status ──

export async function pullStatus(
	_cwd: string,
	config: RtimeConfig,
	taskId?: string,
): Promise<{
	logs: Array<{
		backlogTaskId: string;
		hours: number;
		skill: string;
		status: string;
		commitmentId?: string;
	}>;
	error?: string;
}> {
	const params = new URLSearchParams();
	if (config.memberName) params.set("memberName", config.memberName);
	if (taskId) params.set("backlogTaskId", taskId);

	const url = getApiUrl(config, `/api/external-time-logs?${params.toString()}`);

	try {
		const resp = await fetch(url, {
			headers: getAuthHeaders(config),
			signal: AbortSignal.timeout(10_000),
		});

		if (!resp.ok) {
			return { logs: [], error: `HTTP ${resp.status}` };
		}

		const data = (await resp.json()) as {
			logs: Array<{
				backlogTaskId: string;
				hours: number;
				skill: string;
				status: string;
				commitmentId?: string;
			}>;
		};
		return { logs: data.logs };
	} catch (err) {
		return { logs: [], error: err instanceof Error ? err.message : String(err) };
	}
}

// ── Ensure rTime task ──

export async function ensureRtimeTask(task: Task, config: RtimeConfig): Promise<{ taskId?: string; error?: string }> {
	const url = getApiUrl(config, "/api/tasks");

	try {
		// Check if a task already references this backlog task
		const listResp = await fetch(url, {
			headers: getAuthHeaders(config),
			signal: AbortSignal.timeout(10_000),
		});

		if (listResp.ok) {
			const data = (await listResp.json()) as { tasks: Array<{ id: string; description: string; name: string }> };
			const existing = data.tasks.find(
				(t) => t.description?.includes(`[backlog:${task.id}]`) || t.name?.includes(task.id),
			);
			if (existing) return { taskId: existing.id };
		}

		// Create new task
		const createResp = await fetch(url, {
			method: "POST",
			headers: getAuthHeaders(config),
			body: JSON.stringify({
				name: task.title,
				description: `[backlog:${task.id}]`,
				needs: {},
			}),
			signal: AbortSignal.timeout(10_000),
		});

		if (!createResp.ok) {
			return { error: `Failed to create rTime task: HTTP ${createResp.status}` };
		}

		const result = (await createResp.json()) as { id: string };
		return { taskId: result.id };
	} catch (err) {
		return { error: err instanceof Error ? err.message : String(err) };
	}
}

// ── Config loader ──

export function getRtimeConfig(backlogConfig: BacklogConfig | null): RtimeConfig | undefined {
	const config = backlogConfig as any;
	return config?.rtime;
}
