import type { Database } from 'bun:sqlite';
import type {
	TaskStorage,
	Task,
	TaskChangelogEntry,
	TaskPriority,
	TaskStatus,
	TaskType,
	CreateTaskParams,
	UpdateTaskParams,
	ListTasksParams,
	ListTasksResult,
	BatchDeleteTasksParams,
	BatchDeleteTasksResult,
	BatchUpdateTasksParams,
	BatchUpdateTasksResult,
	BatchCloseTasksParams,
	BatchCloseTasksResult,
	TaskChangelogResult,
	Comment,
	Tag,
	ListCommentsResult,
	ListTagsResult,
	ListUsersResult,
	ListProjectsResult,
	Attachment,
	CreateAttachmentParams,
	PresignUploadResponse,
	PresignDownloadResponse,
	ListAttachmentsResult,
	TaskActivityParams,
	TaskActivityResult,
	UserEntityRef,
	EntityRef,
} from '@agentuity/core';
import { StructuredError, normalizeTaskStatus } from '@agentuity/core';
import { now } from './_util';

const TaskTitleRequiredError = StructuredError(
	'TaskTitleRequiredError',
	'Task title is required and must be a non-empty string'
);

const TaskNotFoundError = StructuredError('TaskNotFoundError', 'Task not found');

const TaskAlreadyClosedError = StructuredError('TaskAlreadyClosedError', 'Task is already closed');

const CommentNotFoundError = StructuredError('CommentNotFoundError', 'Comment not found');

const TagNotFoundError = StructuredError('TagNotFoundError', 'Tag not found');

const CommentBodyRequiredError = StructuredError(
	'CommentBodyRequiredError',
	'Comment body is required and must be a non-empty string'
);

const CommentUserRequiredError = StructuredError(
	'CommentUserRequiredError',
	'Comment user ID is required and must be a non-empty string'
);

const TagNameRequiredError = StructuredError(
	'TagNameRequiredError',
	'Tag name is required and must be a non-empty string'
);

const AttachmentNotSupportedError = StructuredError(
	'AttachmentNotSupportedError',
	'Attachments are not supported in local task storage'
);

const UserNotFoundError = StructuredError('UserNotFoundError', 'User not found');

const UserNameRequiredError = StructuredError(
	'UserNameRequiredError',
	'User name is required and must be a non-empty string'
);

const ProjectNotFoundError = StructuredError('ProjectNotFoundError', 'Project not found');

const ProjectNameRequiredError = StructuredError(
	'ProjectNameRequiredError',
	'Project name is required and must be a non-empty string'
);

type CommentRow = {
	id: string;
	created_at: number;
	updated_at: number;
	task_id: string;
	user_id: string;
	body: string;
};

type TagRow = {
	id: string;
	created_at: number;
	name: string;
	color: string | null;
};

type TaskRow = {
	id: string;
	created_at: number;
	updated_at: number;
	title: string;
	description: string | null;
	metadata: string | null;
	priority: TaskPriority;
	parent_id: string | null;
	type: TaskType;
	status: TaskStatus;
	open_date: string | null;
	in_progress_date: string | null;
	closed_date: string | null;
	created_id: string;
	assigned_id: string | null;
	closed_id: string | null;
	deleted: number;
};

type TaskChangelogRow = {
	id: string;
	created_at: number;
	task_id: string;
	field: string;
	old_value: string | null;
	new_value: string | null;
};

const DEFAULT_LIMIT = 100;

const SORT_FIELDS: Record<string, string> = {
	created_at: 'created_at',
	updated_at: 'updated_at',
	title: 'title',
	priority: 'priority',
	status: 'status',
	type: 'type',
	open_date: 'open_date',
	in_progress_date: 'in_progress_date',
	closed_date: 'closed_date',
};

const DURATION_UNITS: Record<string, number> = {
	s: 1000,
	m: 60 * 1000,
	h: 60 * 60 * 1000,
	d: 24 * 60 * 60 * 1000,
	w: 7 * 24 * 60 * 60 * 1000,
};

const InvalidDurationError = StructuredError(
	'InvalidDurationError',
	'Invalid duration format: use a number followed by s (seconds), m (minutes), h (hours), d (days), or w (weeks)'
);

function parseDurationMs(duration: string): number {
	const match = duration.match(/^(\d+)([smhdw])$/);
	if (!match) {
		throw new InvalidDurationError();
	}
	const value = parseInt(match[1]!, 10);
	const unit = match[2]!;
	const ms = DURATION_UNITS[unit];
	if (!ms) {
		throw new InvalidDurationError();
	}
	return value * ms;
}

function generateTaskId(): string {
	return `task_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function generateChangelogId(): string {
	return `taskch_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function toTask(row: TaskRow): Task {
	return {
		id: row.id,
		created_at: new Date(row.created_at).toISOString(),
		updated_at: new Date(row.updated_at).toISOString(),
		title: row.title,
		description: row.description ?? undefined,
		metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, unknown>) : undefined,
		priority: row.priority,
		parent_id: row.parent_id ?? undefined,
		type: row.type,
		status: row.status,
		open_date: row.open_date ?? undefined,
		in_progress_date: row.in_progress_date ?? undefined,
		closed_date: row.closed_date ?? undefined,
		created_id: row.created_id,
		assigned_id: row.assigned_id ?? undefined,
		closed_id: row.closed_id ?? undefined,
		deleted: row.deleted === 1,
	};
}

function toComment(row: CommentRow): Comment {
	return {
		id: row.id,
		created_at: new Date(row.created_at).toISOString(),
		updated_at: new Date(row.updated_at).toISOString(),
		task_id: row.task_id,
		user_id: row.user_id,
		body: row.body,
	};
}

function toTag(row: TagRow): Tag {
	return {
		id: row.id,
		created_at: new Date(row.created_at).toISOString(),
		name: row.name,
		color: row.color ?? undefined,
	};
}

function generateCommentId(): string {
	return `comment_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function generateTagId(): string {
	return `tag_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function generateUserId(): string {
	return `usr_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function generateProjectId(): string {
	return `prj_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}

function toChangelogEntry(row: TaskChangelogRow): TaskChangelogEntry {
	return {
		id: row.id,
		created_at: new Date(row.created_at).toISOString(),
		task_id: row.task_id,
		field: row.field,
		old_value: row.old_value ?? undefined,
		new_value: row.new_value ?? undefined,
	};
}

export class LocalTaskStorage implements TaskStorage {
	#db: Database;
	#projectPath: string;

	constructor(db: Database, projectPath: string) {
		this.#db = db;
		this.#projectPath = projectPath;
	}

	async create(params: CreateTaskParams): Promise<Task> {
		const trimmedTitle = params?.title?.trim();
		if (!trimmedTitle) {
			throw new TaskTitleRequiredError();
		}

		const id = generateTaskId();
		const timestamp = now();
		const status: TaskStatus = params.status ? normalizeTaskStatus(params.status) : 'open';
		const priority: TaskPriority = params.priority ?? 'none';
		const openDate = status === 'open' ? new Date(timestamp).toISOString() : null;
		const inProgressDate = status === 'in_progress' ? new Date(timestamp).toISOString() : null;
		const closedDate = status === 'done' ? new Date(timestamp).toISOString() : null;

		const stmt = this.#db.prepare(`
			INSERT INTO task_storage (
				project_path,
				id,
				title,
				description,
				metadata,
				priority,
				parent_id,
				type,
				status,
				open_date,
				in_progress_date,
				closed_date,
				created_id,
				assigned_id,
				closed_id,
				created_at,
				updated_at
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`);

		const row: TaskRow = {
			id,
			created_at: timestamp,
			updated_at: timestamp,
			title: trimmedTitle,
			description: params.description ?? null,
			metadata: params.metadata ? JSON.stringify(params.metadata) : null,
			priority,
			parent_id: params.parent_id ?? null,
			type: params.type,
			status,
			open_date: openDate,
			in_progress_date: inProgressDate,
			closed_date: closedDate,
			created_id: params.created_id,
			assigned_id: params.assigned_id ?? null,
			closed_id: null,
			deleted: 0,
		};

		stmt.run(
			this.#projectPath,
			row.id,
			row.title,
			row.description,
			row.metadata,
			row.priority,
			row.parent_id,
			row.type,
			row.status,
			row.open_date,
			row.in_progress_date,
			row.closed_date,
			row.created_id,
			row.assigned_id,
			row.closed_id,
			row.created_at,
			row.updated_at
		);

		return toTask(row);
	}

	async get(id: string): Promise<Task | null> {
		const query = this.#db.query(`
			SELECT
				id,
				created_at,
				updated_at,
				title,
				description,
				metadata,
				priority,
				parent_id,
				type,
				status,
				open_date,
				in_progress_date,
				closed_date,
				created_id,
				assigned_id,
				closed_id,
				deleted
			FROM task_storage
			WHERE project_path = ? AND id = ?
		`);

		const row = query.get(this.#projectPath, id) as TaskRow | null;
		if (!row) {
			return null;
		}

		return toTask(row);
	}

	async list(params?: ListTasksParams): Promise<ListTasksResult> {
		const filters: string[] = ['project_path = ?'];
		const values: Array<string | number> = [this.#projectPath];

		if (params?.status) {
			filters.push('status = ?');
			values.push(normalizeTaskStatus(params.status));
		}
		if (params?.type) {
			filters.push('type = ?');
			values.push(params.type);
		}
		if (params?.priority) {
			filters.push('priority = ?');
			values.push(params.priority);
		}
		if (params?.assigned_id) {
			filters.push('assigned_id = ?');
			values.push(params.assigned_id);
		}
		if (params?.parent_id) {
			filters.push('parent_id = ?');
			values.push(params.parent_id);
		}
		if (params?.created_id) {
			filters.push('created_id = ?');
			values.push(params.created_id);
		}
		if (params?.project_id) {
			filters.push('project_id = ?');
			values.push(params.project_id);
		}
		if (params?.deleted === undefined) {
			filters.push('deleted = 0');
		} else {
			filters.push('deleted = ?');
			values.push(params.deleted ? 1 : 0);
		}
		if (params?.tag_id) {
			filters.push(
				'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
			);
			values.push(params.tag_id);
			values.push(this.#projectPath);
		}

		const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
		const sortField =
			params?.sort && SORT_FIELDS[params.sort] ? SORT_FIELDS[params.sort] : 'created_at';
		const sortOrder = params?.order === 'asc' ? 'ASC' : 'DESC';
		const limit = params?.limit ?? DEFAULT_LIMIT;
		const offset = params?.offset ?? 0;

		const totalQuery = this.#db.query(
			`SELECT COUNT(*) as count FROM task_storage ${whereClause}`
		);
		const totalRow = totalQuery.get(...values) as { count: number };

		const query = this.#db.query(`
			SELECT
				id,
				created_at,
				updated_at,
				title,
				description,
				metadata,
				priority,
				parent_id,
				type,
				status,
				open_date,
				in_progress_date,
				closed_date,
				created_id,
				assigned_id,
				closed_id,
				deleted
			FROM task_storage
			${whereClause}
			ORDER BY ${sortField} ${sortOrder}
			LIMIT ? OFFSET ?
		`);

		const rows = query.all(...values, limit, offset) as TaskRow[];

		return {
			tasks: rows.map(toTask),
			total: totalRow.count,
			limit,
			offset,
		};
	}

	async update(id: string, params: UpdateTaskParams): Promise<Task> {
		const updateInTransaction = this.#db.transaction(() => {
			const existingQuery = this.#db.query(`
				SELECT
					id,
					created_at,
					updated_at,
					title,
					description,
					metadata,
					priority,
					parent_id,
					type,
					status,
					open_date,
					in_progress_date,
					closed_date,
					created_id,
					assigned_id,
					closed_id,
					deleted
				FROM task_storage
				WHERE project_path = ? AND id = ?
			`);

			const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
			if (!existing) {
				throw new TaskNotFoundError();
			}
			const trimmedTitle = params.title !== undefined ? params.title?.trim() : undefined;
			if (params.title !== undefined && !trimmedTitle) {
				throw new TaskTitleRequiredError();
			}
			const timestamp = now();
			const nowIso = new Date(timestamp).toISOString();
			const normalizedStatus = params.status ? normalizeTaskStatus(params.status) : undefined;

			const updated: TaskRow = {
				...existing,
				title: trimmedTitle ?? existing.title,
				description:
					params.description !== undefined ? params.description : existing.description,
				metadata:
					params.metadata !== undefined
						? params.metadata
							? JSON.stringify(params.metadata)
							: null
						: existing.metadata,
				priority: params.priority ?? existing.priority,
				parent_id: params.parent_id !== undefined ? params.parent_id : existing.parent_id,
				type: params.type ?? existing.type,
				status: normalizedStatus ?? existing.status,
				assigned_id:
					params.assigned_id !== undefined ? params.assigned_id : existing.assigned_id,
				closed_id: params.closed_id !== undefined ? params.closed_id : existing.closed_id,
				updated_at: timestamp,
			};

			if (normalizedStatus && normalizedStatus !== existing.status) {
				if (normalizedStatus === 'open' && !existing.open_date) {
					updated.open_date = nowIso;
				}
				if (normalizedStatus === 'in_progress' && !existing.in_progress_date) {
					updated.in_progress_date = nowIso;
				}
				if (normalizedStatus === 'done' && !existing.closed_date) {
					updated.closed_date = nowIso;
				}
			}

			const changelogEntries: Array<{
				field: string;
				oldValue: string | null;
				newValue: string | null;
			}> = [];

			const compare = (field: string, oldValue: string | null, newValue: string | null) => {
				if (oldValue !== newValue) {
					changelogEntries.push({ field, oldValue, newValue });
				}
			};

			if (params.title !== undefined) {
				compare('title', existing.title, updated.title);
			}
			if (params.description !== undefined) {
				compare('description', existing.description, updated.description);
			}
			if (params.metadata !== undefined) {
				compare('metadata', existing.metadata, updated.metadata);
			}
			if (params.priority !== undefined) {
				compare('priority', existing.priority, updated.priority);
			}
			if (params.parent_id !== undefined) {
				compare('parent_id', existing.parent_id, updated.parent_id);
			}
			if (params.type !== undefined) {
				compare('type', existing.type, updated.type);
			}
			if (normalizedStatus !== undefined) {
				compare('status', existing.status, updated.status);
			}
			if (params.assigned_id !== undefined) {
				compare('assigned_id', existing.assigned_id, updated.assigned_id);
			}
			if (params.closed_id !== undefined) {
				compare('closed_id', existing.closed_id, updated.closed_id);
			}

			const updateStmt = this.#db.prepare(`
				UPDATE task_storage
				SET
					title = ?,
					description = ?,
					metadata = ?,
					priority = ?,
					parent_id = ?,
					type = ?,
					status = ?,
					open_date = ?,
					in_progress_date = ?,
					closed_date = ?,
					created_id = ?,
					assigned_id = ?,
					closed_id = ?,
					updated_at = ?
				WHERE project_path = ? AND id = ?
			`);

			updateStmt.run(
				updated.title,
				updated.description,
				updated.metadata,
				updated.priority,
				updated.parent_id,
				updated.type,
				updated.status,
				updated.open_date,
				updated.in_progress_date,
				updated.closed_date,
				updated.created_id,
				updated.assigned_id,
				updated.closed_id,
				updated.updated_at,
				this.#projectPath,
				id
			);

			if (changelogEntries.length > 0) {
				const changelogStmt = this.#db.prepare(`
					INSERT INTO task_changelog_storage (
						project_path,
						id,
						task_id,
						field,
						old_value,
						new_value,
						created_at
					) VALUES (?, ?, ?, ?, ?, ?, ?)
				`);

				for (const entry of changelogEntries) {
					changelogStmt.run(
						this.#projectPath,
						generateChangelogId(),
						id,
						entry.field,
						entry.oldValue,
						entry.newValue,
						timestamp
					);
				}
			}

			return toTask(updated);
		});

		return updateInTransaction.immediate();
	}

	async close(id: string): Promise<Task> {
		const closeInTransaction = this.#db.transaction(() => {
			const existingQuery = this.#db.query(`
				SELECT
					id,
					created_at,
					updated_at,
					title,
					description,
					metadata,
					priority,
					parent_id,
					type,
					status,
					open_date,
					in_progress_date,
					closed_date,
					created_id,
					assigned_id,
					closed_id,
					deleted
				FROM task_storage
				WHERE project_path = ? AND id = ?
			`);

			const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
			if (!existing) {
				throw new TaskNotFoundError();
			}

			if (existing.status === 'done') {
				throw new TaskAlreadyClosedError();
			}
			const timestamp = now();
			const nowIso = new Date(timestamp).toISOString();
			const updated: TaskRow = {
				...existing,
				status: 'done',
				closed_date: existing.closed_date ?? nowIso,
				updated_at: timestamp,
			};

			const updateStmt = this.#db.prepare(`
				UPDATE task_storage
				SET status = ?, closed_date = ?, updated_at = ?
				WHERE project_path = ? AND id = ?
			`);

			updateStmt.run(
				updated.status,
				updated.closed_date,
				updated.updated_at,
				this.#projectPath,
				id
			);

			const changelogStmt = this.#db.prepare(`
				INSERT INTO task_changelog_storage (
					project_path,
					id,
					task_id,
					field,
					old_value,
					new_value,
					created_at
				) VALUES (?, ?, ?, ?, ?, ?, ?)
			`);

			changelogStmt.run(
				this.#projectPath,
				generateChangelogId(),
				id,
				'status',
				existing.status,
				updated.status,
				timestamp
			);

			return toTask(updated);
		});

		return closeInTransaction.immediate();
	}

	async changelog(
		id: string,
		params?: { limit?: number; offset?: number }
	): Promise<TaskChangelogResult> {
		const limit = params?.limit ?? DEFAULT_LIMIT;
		const offset = params?.offset ?? 0;

		const totalQuery = this.#db.query(
			`SELECT COUNT(*) as count FROM task_changelog_storage WHERE project_path = ? AND task_id = ?`
		);
		const totalRow = totalQuery.get(this.#projectPath, id) as { count: number };

		const query = this.#db.query(`
			SELECT
				id,
				created_at,
				task_id,
				field,
				old_value,
				new_value
			FROM task_changelog_storage
			WHERE project_path = ? AND task_id = ?
			ORDER BY created_at DESC
			LIMIT ? OFFSET ?
		`);

		const rows = query.all(this.#projectPath, id, limit, offset) as TaskChangelogRow[];

		return {
			changelog: rows.map(toChangelogEntry),
			total: totalRow.count,
			limit,
			offset,
		};
	}

	async softDelete(id: string): Promise<Task> {
		const task = await this.get(id);
		if (!task) {
			throw new TaskNotFoundError();
		}

		const timestamp = now();

		const updateStmt = this.#db.prepare(`
			UPDATE task_storage
		SET status = 'done', deleted = 1, closed_date = COALESCE(closed_date, ?), updated_at = ?
		WHERE project_path = ? AND id = ?
	`);

		updateStmt.run(new Date(timestamp).toISOString(), timestamp, this.#projectPath, id);

		const changelogStmt = this.#db.prepare(`
			INSERT INTO task_changelog_storage (
				project_path, id, task_id, field, old_value, new_value, created_at
			) VALUES (?, ?, ?, ?, ?, ?, ?)
		`);

		changelogStmt.run(
			this.#projectPath,
			generateChangelogId(),
			id,
			'deleted',
			'false',
			'true',
			timestamp
		);

		const updated = await this.get(id);
		return updated!;
	}

	async batchDelete(params: BatchDeleteTasksParams): Promise<BatchDeleteTasksResult> {
		const conditions: string[] = ['project_path = ?', 'deleted = 0'];
		const args: (string | number)[] = [this.#projectPath];

		if (params.status) {
			conditions.push('status = ?');
			args.push(normalizeTaskStatus(params.status));
		}
		if (params.type) {
			conditions.push('type = ?');
			args.push(params.type);
		}
		if (params.priority) {
			conditions.push('priority = ?');
			args.push(params.priority);
		}
		if (params.parent_id) {
			conditions.push('parent_id = ?');
			args.push(params.parent_id);
		}
		if (params.created_id) {
			conditions.push('created_id = ?');
			args.push(params.created_id);
		}
		if (params.older_than) {
			const ms = parseDurationMs(params.older_than);
			const cutoff = Date.now() - ms;
			conditions.push('created_at < ?');
			args.push(cutoff);
		}

		// Require at least one filter beyond project_path + deleted
		if (conditions.length < 3) {
			const BatchDeleteFilterRequiredError = StructuredError(
				'BatchDeleteFilterRequiredError',
				'At least one filter is required for batch delete'
			);
			throw new BatchDeleteFilterRequiredError();
		}

		if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit <= 0)) {
			const InvalidBatchDeleteLimitError = StructuredError(
				'InvalidBatchDeleteLimitError',
				'Batch delete limit must be a positive integer'
			);
			throw new InvalidBatchDeleteLimitError();
		}
		const limit = Math.min(params.limit ?? 50, 200);

		const whereClause = conditions.join(' AND ');
		const selectQuery = `SELECT id, title FROM task_storage WHERE ${whereClause} ORDER BY created_at ASC LIMIT ?`;
		const selectStmt = this.#db.prepare(selectQuery);
		const rows = selectStmt.all(...args, limit) as Array<{ id: string; title: string }>;

		if (rows.length === 0) {
			return { deleted: [], count: 0 };
		}

		const timestamp = now();
		const ids = rows.map((r) => r.id);
		const placeholders = ids.map(() => '?').join(', ');

		const txn = this.#db.transaction(() => {
			const updateStmt = this.#db.prepare(`
				UPDATE task_storage
			SET status = 'done', deleted = 1, closed_date = COALESCE(closed_date, ?), updated_at = ?
			WHERE project_path = ? AND id IN (${placeholders})
			`);
			updateStmt.run(new Date(timestamp).toISOString(), timestamp, this.#projectPath, ...ids);

			const changelogStmt = this.#db.prepare(`
				INSERT INTO task_changelog_storage (
					project_path, id, task_id, field, old_value, new_value, created_at
				) VALUES (?, ?, ?, ?, ?, ?, ?)
			`);
			for (const row of rows) {
				changelogStmt.run(
					this.#projectPath,
					generateChangelogId(),
					row.id,
					'deleted',
					'false',
					'true',
					timestamp
				);
			}
		});
		txn();

		return {
			deleted: rows.map((r) => ({ id: r.id, title: r.title })),
			count: rows.length,
		};
	}

	async batchUpdate(params: BatchUpdateTasksParams): Promise<BatchUpdateTasksResult> {
		const conditions: string[] = ['project_path = ?', 'deleted = 0'];
		const args: (string | number)[] = [this.#projectPath];

		// Handle explicit IDs
		if (params.ids && params.ids.length > 0) {
			const placeholders = params.ids.map(() => '?').join(', ');
			conditions.push(`id IN (${placeholders})`);
			args.push(...params.ids);
		} else {
			// Build filter conditions
			if (params.status) {
				conditions.push('status = ?');
				args.push(normalizeTaskStatus(params.status));
			}
			if (params.type) {
				conditions.push('type = ?');
				args.push(params.type);
			}
			if (params.priority) {
				conditions.push('priority = ?');
				args.push(params.priority);
			}
			if (params.parent_id) {
				conditions.push('parent_id = ?');
				args.push(params.parent_id);
			}
			if (params.created_id) {
				conditions.push('created_id = ?');
				args.push(params.created_id);
			}
			if (params.assigned_id) {
				conditions.push('assigned_id = ?');
				args.push(params.assigned_id);
			}
			if (params.older_than) {
				const ms = parseDurationMs(params.older_than);
				const cutoff = Date.now() - ms;
				conditions.push('created_at < ?');
				args.push(cutoff);
			}
			if (params.project_id) {
				conditions.push('project_id = ?');
				args.push(params.project_id);
			}
			if (params.tag_id) {
				conditions.push(
					'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
				);
				args.push(params.tag_id);
				args.push(this.#projectPath);
			}
			if (params.newer_than) {
				const ms = parseDurationMs(params.newer_than);
				const cutoff = Date.now() - ms;
				conditions.push('created_at > ?');
				args.push(cutoff);
			}
		}

		// Require at least one filter or IDs
		if (params.ids && params.ids.length > 0) {
			// IDs provided, OK
		} else if (conditions.length < 3) {
			throw new Error('At least one filter or ids is required for batch update');
		}

		// Check for update fields
		const hasUpdate =
			params.new_status ||
			params.new_priority ||
			params.new_assigned_id ||
			params.new_assignee ||
			params.new_title ||
			params.new_description ||
			params.new_metadata ||
			params.new_type;
		if (!hasUpdate) {
			throw new Error('At least one update field is required for batch update');
		}

		if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit <= 0)) {
			throw new Error('Batch update limit must be a positive integer');
		}
		const limit = Math.min(params.limit ?? 50, 200);

		const whereClause = conditions.join(' AND ');
		const selectQuery = `SELECT id, title, status, priority FROM task_storage WHERE ${whereClause} ORDER BY created_at ASC LIMIT ?`;
		const selectStmt = this.#db.prepare(selectQuery);
		const rows = selectStmt.all(...args, limit) as Array<{
			id: string;
			title: string;
			status: string;
			priority: string;
		}>;

		if (rows.length === 0) {
			return { updated: [], count: 0, dry_run: params.dry_run ?? false };
		}

		// Dry run - return preview without updating
		if (params.dry_run) {
			const normalizedStatus = params.new_status
				? normalizeTaskStatus(params.new_status)
				: undefined;
			return {
				updated: rows.map((r) => ({
					id: r.id,
					title: params.new_title ?? r.title,
					status: (normalizedStatus ?? r.status) as TaskStatus,
					priority: (params.new_priority ?? r.priority) as TaskPriority,
				})),
				count: rows.length,
				dry_run: true,
			};
		}

		const timestamp = now();
		const ids = rows.map((r) => r.id);
		const placeholders = ids.map(() => '?').join(', ');

		const updateFields: string[] = ['updated_at = ?'];
		const updateArgs: (string | number)[] = [timestamp];

		if (params.new_status) {
			updateFields.push('status = ?');
			updateArgs.push(normalizeTaskStatus(params.new_status));
		}
		if (params.new_priority) {
			updateFields.push('priority = ?');
			updateArgs.push(params.new_priority);
		}
		if (params.new_assigned_id) {
			updateFields.push('assigned_id = ?');
			updateArgs.push(params.new_assigned_id);
		}
		if (params.new_title) {
			updateFields.push('title = ?');
			updateArgs.push(params.new_title);
		}
		if (params.new_description) {
			updateFields.push('description = ?');
			updateArgs.push(params.new_description);
		}
		if (params.new_metadata) {
			updateFields.push('metadata = ?');
			updateArgs.push(JSON.stringify(params.new_metadata));
		}
		if (params.new_type) {
			updateFields.push('type = ?');
			updateArgs.push(params.new_type);
		}

		// Set lifecycle timestamps based on new status (only when transitioning)
		if (params.new_status) {
			const newStatus = normalizeTaskStatus(params.new_status);
			if (newStatus === 'open') {
				updateFields.push('open_date = COALESCE(open_date, ?)');
				updateArgs.push(new Date(timestamp).toISOString());
			} else if (newStatus === 'in_progress') {
				updateFields.push('in_progress_date = COALESCE(in_progress_date, ?)');
				updateArgs.push(new Date(timestamp).toISOString());
			} else if (newStatus === 'done') {
				updateFields.push('closed_date = COALESCE(closed_date, ?)');
				updateArgs.push(new Date(timestamp).toISOString());
			}
		}

		const txn = this.#db.transaction(() => {
			const updateStmt = this.#db.prepare(`
				UPDATE task_storage SET ${updateFields.join(', ')}
				WHERE project_path = ? AND id IN (${placeholders})
			`);
			updateStmt.run(...updateArgs, this.#projectPath, ...ids);

			const changelogStmt = this.#db.prepare(`
				INSERT INTO task_changelog_storage (
					project_path, id, task_id, field, old_value, new_value, created_at
				) VALUES (?, ?, ?, ?, ?, ?, ?)
			`);
			for (const row of rows) {
				if (params.new_status && row.status !== params.new_status) {
					changelogStmt.run(
						this.#projectPath,
						generateChangelogId(),
						row.id,
						'status',
						row.status,
						params.new_status,
						timestamp
					);
				}
				if (params.new_priority && row.priority !== params.new_priority) {
					changelogStmt.run(
						this.#projectPath,
						generateChangelogId(),
						row.id,
						'priority',
						row.priority,
						params.new_priority,
						timestamp
					);
				}
			}
		});
		txn();

		return {
			updated: rows.map((r) => ({
				id: r.id,
				title: params.new_title ?? r.title,
				status: (params.new_status ?? r.status) as TaskStatus,
				priority: (params.new_priority ?? r.priority) as TaskPriority,
			})),
			count: rows.length,
			dry_run: false,
		};
	}

	async batchClose(params: BatchCloseTasksParams): Promise<BatchCloseTasksResult> {
		// Resolve closer ID from either closed_id or closer entity ref
		const closerId = params.closed_id ?? params.closer?.id ?? null;

		const conditions: string[] = ['project_path = ?', 'deleted = 0', "status != 'done'"];
		const args: (string | number)[] = [this.#projectPath];

		// Handle explicit IDs
		if (params.ids && params.ids.length > 0) {
			const placeholders = params.ids.map(() => '?').join(', ');
			conditions.push(`id IN (${placeholders})`);
			args.push(...params.ids);
		} else {
			// Build filter conditions
			if (params.status) {
				conditions.push('status = ?');
				args.push(normalizeTaskStatus(params.status));
			}
			if (params.type) {
				conditions.push('type = ?');
				args.push(params.type);
			}
			if (params.priority) {
				conditions.push('priority = ?');
				args.push(params.priority);
			}
			if (params.parent_id) {
				conditions.push('parent_id = ?');
				args.push(params.parent_id);
			}
			if (params.created_id) {
				conditions.push('created_id = ?');
				args.push(params.created_id);
			}
			if (params.assigned_id) {
				conditions.push('assigned_id = ?');
				args.push(params.assigned_id);
			}
			if (params.older_than) {
				const ms = parseDurationMs(params.older_than);
				const cutoff = Date.now() - ms;
				conditions.push('created_at < ?');
				args.push(cutoff);
			}
			if (params.project_id) {
				conditions.push('project_id = ?');
				args.push(params.project_id);
			}
			if (params.tag_id) {
				conditions.push(
					'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
				);
				args.push(params.tag_id);
				args.push(this.#projectPath);
			}
			if (params.newer_than) {
				const ms = parseDurationMs(params.newer_than);
				const cutoff = Date.now() - ms;
				conditions.push('created_at > ?');
				args.push(cutoff);
			}
		}

		// Require at least one filter or IDs
		if (params.ids && params.ids.length > 0) {
			// IDs provided, OK
		} else if (conditions.length < 4) {
			throw new Error('At least one filter or ids is required for batch close');
		}

		if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit <= 0)) {
			throw new Error('Batch close limit must be a positive integer');
		}
		const limit = Math.min(params.limit ?? 50, 200);

		const whereClause = conditions.join(' AND ');
		const selectQuery = `SELECT id, title, status FROM task_storage WHERE ${whereClause} ORDER BY created_at ASC LIMIT ?`;
		const selectStmt = this.#db.prepare(selectQuery);
		const rows = selectStmt.all(...args, limit) as Array<{
			id: string;
			title: string;
			status: string;
		}>;

		if (rows.length === 0) {
			return { closed: [], count: 0, dry_run: params.dry_run ?? false };
		}

		// Dry run - return preview without closing
		if (params.dry_run) {
			const nowTs = new Date().toISOString();
			return {
				closed: rows.map((r) => ({
					id: r.id,
					title: r.title,
					status: 'done',
					closed_date: nowTs,
				})),
				count: rows.length,
				dry_run: true,
			};
		}

		const timestamp = now();
		const ids = rows.map((r) => r.id);
		const placeholders = ids.map(() => '?').join(', ');
		const closedDate = new Date(timestamp).toISOString();

		const txn = this.#db.transaction(() => {
			const updateStmt = this.#db.prepare(`
				UPDATE task_storage SET status = 'done', closed_date = COALESCE(closed_date, ?), closed_id = ?, updated_at = ?
				WHERE project_path = ? AND id IN (${placeholders})
			`);
			updateStmt.run(closedDate, closerId, timestamp, this.#projectPath, ...ids);

			const changelogStmt = this.#db.prepare(`
				INSERT INTO task_changelog_storage (
					project_path, id, task_id, field, old_value, new_value, created_at
				) VALUES (?, ?, ?, ?, ?, ?, ?)
			`);
			for (const row of rows) {
				if (row.status !== 'done') {
					changelogStmt.run(
						this.#projectPath,
						generateChangelogId(),
						row.id,
						'status',
						row.status,
						'done',
						timestamp
					);
				}
			}
		});
		txn();

		return {
			closed: rows.map((r) => ({
				id: r.id,
				title: r.title,
				status: 'done',
				closed_date: closedDate,
			})),
			count: rows.length,
			dry_run: false,
		};
	}

	async createComment(taskId: string, body: string, userId: string): Promise<Comment> {
		const trimmedBody = body?.trim();
		if (!trimmedBody) {
			throw new CommentBodyRequiredError();
		}

		const trimmedUserId = userId?.trim();
		if (!trimmedUserId) {
			throw new CommentUserRequiredError();
		}

		const task = await this.get(taskId);
		if (!task) {
			throw new TaskNotFoundError();
		}

		const id = generateCommentId();
		const timestamp = now();

		const stmt = this.#db.prepare(`
			INSERT INTO task_comment_storage (
				project_path, id, task_id, user_id, body, created_at, updated_at
			) VALUES (?, ?, ?, ?, ?, ?, ?)
		`);

		stmt.run(this.#projectPath, id, taskId, trimmedUserId, trimmedBody, timestamp, timestamp);

		return toComment({
			id,
			created_at: timestamp,
			updated_at: timestamp,
			task_id: taskId,
			user_id: trimmedUserId,
			body: trimmedBody,
		});
	}

	async getComment(commentId: string): Promise<Comment> {
		const query = this.#db.query(`
			SELECT id, created_at, updated_at, task_id, user_id, body
			FROM task_comment_storage
			WHERE project_path = ? AND id = ?
		`);

		const row = query.get(this.#projectPath, commentId) as CommentRow | null;
		if (!row) {
			throw new CommentNotFoundError();
		}

		return toComment(row);
	}

	async updateComment(commentId: string, body: string): Promise<Comment> {
		const trimmedBody = body?.trim();
		if (!trimmedBody) {
			throw new CommentBodyRequiredError();
		}

		const existing = await this.getComment(commentId);
		const timestamp = now();

		const stmt = this.#db.prepare(`
			UPDATE task_comment_storage
			SET body = ?, updated_at = ?
			WHERE project_path = ? AND id = ?
		`);

		stmt.run(trimmedBody, timestamp, this.#projectPath, commentId);

		return {
			...existing,
			body: trimmedBody,
			updated_at: new Date(timestamp).toISOString(),
		};
	}

	async deleteComment(commentId: string): Promise<void> {
		const stmt = this.#db.prepare(`
			DELETE FROM task_comment_storage
			WHERE project_path = ? AND id = ?
		`);

		stmt.run(this.#projectPath, commentId);
	}

	async listComments(
		taskId: string,
		params?: { limit?: number; offset?: number }
	): Promise<ListCommentsResult> {
		const limit = params?.limit ?? DEFAULT_LIMIT;
		const offset = params?.offset ?? 0;

		const totalQuery = this.#db.query(
			`SELECT COUNT(*) as count FROM task_comment_storage WHERE project_path = ? AND task_id = ?`
		);
		const totalRow = totalQuery.get(this.#projectPath, taskId) as { count: number };

		const query = this.#db.query(`
			SELECT id, created_at, updated_at, task_id, user_id, body
			FROM task_comment_storage
			WHERE project_path = ? AND task_id = ?
			ORDER BY created_at DESC
			LIMIT ? OFFSET ?
		`);

		const rows = query.all(this.#projectPath, taskId, limit, offset) as CommentRow[];

		return {
			comments: rows.map(toComment),
			total: totalRow.count,
			limit,
			offset,
		};
	}

	async createTag(name: string, color?: string): Promise<Tag> {
		const trimmedName = name?.trim();
		if (!trimmedName) {
			throw new TagNameRequiredError();
		}

		const id = generateTagId();
		const timestamp = now();

		const stmt = this.#db.prepare(`
			INSERT INTO task_tag_storage (
				project_path, id, name, color, created_at
			) VALUES (?, ?, ?, ?, ?)
		`);

		stmt.run(this.#projectPath, id, trimmedName, color ?? null, timestamp);

		return toTag({
			id,
			created_at: timestamp,
			name: trimmedName,
			color: color ?? null,
		});
	}

	async getTag(tagId: string): Promise<Tag> {
		const query = this.#db.query(`
			SELECT id, created_at, name, color
			FROM task_tag_storage
			WHERE project_path = ? AND id = ?
		`);

		const row = query.get(this.#projectPath, tagId) as TagRow | null;
		if (!row) {
			throw new TagNotFoundError();
		}

		return toTag(row);
	}

	async updateTag(tagId: string, name: string, color?: string): Promise<Tag> {
		const trimmedName = name?.trim();
		if (!trimmedName) {
			throw new TagNameRequiredError();
		}

		// Verify exists
		await this.getTag(tagId);

		const stmt = this.#db.prepare(`
			UPDATE task_tag_storage
			SET name = ?, color = ?
			WHERE project_path = ? AND id = ?
		`);

		stmt.run(trimmedName, color ?? null, this.#projectPath, tagId);

		return this.getTag(tagId);
	}

	async deleteTag(tagId: string): Promise<void> {
		// Also remove tag associations
		const deleteAssocStmt = this.#db.prepare(`
			DELETE FROM task_tag_association_storage
			WHERE project_path = ? AND tag_id = ?
		`);
		deleteAssocStmt.run(this.#projectPath, tagId);

		const stmt = this.#db.prepare(`
			DELETE FROM task_tag_storage
			WHERE project_path = ? AND id = ?
		`);
		stmt.run(this.#projectPath, tagId);
	}

	async listTags(): Promise<ListTagsResult> {
		const query = this.#db.query(`
			SELECT id, created_at, name, color
			FROM task_tag_storage
			WHERE project_path = ?
			ORDER BY name ASC
		`);

		const rows = query.all(this.#projectPath) as TagRow[];

		return {
			tags: rows.map(toTag),
		};
	}

	async addTagToTask(taskId: string, tagId: string): Promise<void> {
		// Verify task and tag exist
		const task = await this.get(taskId);
		if (!task) {
			throw new TaskNotFoundError();
		}
		await this.getTag(tagId);

		const stmt = this.#db.prepare(`
			INSERT OR IGNORE INTO task_tag_association_storage (
				project_path, task_id, tag_id
			) VALUES (?, ?, ?)
		`);

		stmt.run(this.#projectPath, taskId, tagId);
	}

	async removeTagFromTask(taskId: string, tagId: string): Promise<void> {
		const stmt = this.#db.prepare(`
			DELETE FROM task_tag_association_storage
			WHERE project_path = ? AND task_id = ? AND tag_id = ?
		`);

		stmt.run(this.#projectPath, taskId, tagId);
	}

	async listTagsForTask(taskId: string): Promise<Tag[]> {
		const query = this.#db.query(`
			SELECT t.id, t.created_at, t.name, t.color
			FROM task_tag_storage t
			INNER JOIN task_tag_association_storage a ON t.id = a.tag_id AND t.project_path = a.project_path
			WHERE a.project_path = ? AND a.task_id = ?
			ORDER BY t.name ASC
		`);

		const rows = query.all(this.#projectPath, taskId) as TagRow[];

		return rows.map(toTag);
	}

	// Attachment methods — not supported in local storage

	async uploadAttachment(
		_taskId: string,
		_params: CreateAttachmentParams
	): Promise<PresignUploadResponse> {
		throw new AttachmentNotSupportedError();
	}

	async confirmAttachment(_attachmentId: string): Promise<Attachment> {
		throw new AttachmentNotSupportedError();
	}

	async downloadAttachment(_attachmentId: string): Promise<PresignDownloadResponse> {
		throw new AttachmentNotSupportedError();
	}

	async listAttachments(_taskId: string): Promise<ListAttachmentsResult> {
		throw new AttachmentNotSupportedError();
	}

	async deleteAttachment(_attachmentId: string): Promise<void> {
		throw new AttachmentNotSupportedError();
	}

	async listUsers(): Promise<ListUsersResult> {
		const query = this.#db.query(`
			SELECT id, name, type
			FROM task_user_storage
			WHERE project_path = ?
			ORDER BY name ASC
		`);

		const rows = query.all(this.#projectPath) as Array<{
			id: string;
			name: string;
			type: 'human' | 'agent';
		}>;

		return {
			users: rows.map((row) => ({
				id: row.id,
				name: row.name,
				type: row.type,
			})),
		};
	}

	async listProjects(): Promise<ListProjectsResult> {
		const query = this.#db.query(`
			SELECT id, name
			FROM task_project_storage
			WHERE project_path = ?
			ORDER BY name ASC
		`);

		const rows = query.all(this.#projectPath) as Array<{
			id: string;
			name: string;
		}>;

		return {
			projects: rows.map((row) => ({
				id: row.id,
				name: row.name,
			})),
		};
	}

	async createUser(params: { name: string; type?: 'human' | 'agent' }): Promise<UserEntityRef> {
		const trimmedName = params?.name?.trim();
		if (!trimmedName) {
			throw new UserNameRequiredError();
		}

		const id = generateUserId();
		const timestamp = now();
		const type = params.type ?? 'human';

		const stmt = this.#db.prepare(`
			INSERT INTO task_user_storage (
				project_path, id, name, type, created_at
			) VALUES (?, ?, ?, ?, ?)
		`);

		stmt.run(this.#projectPath, id, trimmedName, type, timestamp);

		return { id, name: trimmedName, type };
	}

	async getUser(userId: string): Promise<UserEntityRef> {
		const query = this.#db.query(`
			SELECT id, name, type
			FROM task_user_storage
			WHERE project_path = ? AND id = ?
		`);

		const row = query.get(this.#projectPath, userId) as {
			id: string;
			name: string;
			type: 'human' | 'agent';
		} | null;

		if (!row) {
			throw new UserNotFoundError();
		}

		return { id: row.id, name: row.name, type: row.type };
	}

	async deleteUser(userId: string): Promise<void> {
		const stmt = this.#db.prepare(`
			DELETE FROM task_user_storage
			WHERE project_path = ? AND id = ?
		`);

		stmt.run(this.#projectPath, userId);
	}

	async createProject(params: { name: string }): Promise<EntityRef> {
		const trimmedName = params?.name?.trim();
		if (!trimmedName) {
			throw new ProjectNameRequiredError();
		}

		const id = generateProjectId();
		const timestamp = now();

		const stmt = this.#db.prepare(`
			INSERT INTO task_project_storage (
				project_path, id, name, created_at
			) VALUES (?, ?, ?, ?)
		`);

		stmt.run(this.#projectPath, id, trimmedName, timestamp);

		return { id, name: trimmedName };
	}

	async getProject(projectId: string): Promise<EntityRef> {
		const query = this.#db.query(`
			SELECT id, name
			FROM task_project_storage
			WHERE project_path = ? AND id = ?
		`);

		const row = query.get(this.#projectPath, projectId) as {
			id: string;
			name: string;
		} | null;

		if (!row) {
			throw new ProjectNotFoundError();
		}

		return { id: row.id, name: row.name };
	}

	async deleteProject(projectId: string): Promise<void> {
		const stmt = this.#db.prepare(`
			DELETE FROM task_project_storage
			WHERE project_path = ? AND id = ?
		`);

		stmt.run(this.#projectPath, projectId);
	}

	async getActivity(params?: TaskActivityParams): Promise<TaskActivityResult> {
		const days = Math.min(365, Math.max(7, params?.days ?? 90));
		const activity = [];
		const now = new Date();

		for (let i = days - 1; i >= 0; i--) {
			const date = new Date(now);
			date.setDate(date.getDate() - i);
			const dateStr = date.toISOString().slice(0, 10);

			const row = this.#db
				.prepare(
					`SELECT
						COALESCE(SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END), 0) as open,
						COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
					COALESCE(SUM(CASE WHEN status IN ('done', 'closed') THEN 1 ELSE 0 END), 0) as done,
					COALESCE(SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END), 0) as cancelled
					FROM task_storage
					WHERE project_path = ? AND date(created_at) = ?`
				)
				.get(this.#projectPath, dateStr) as {
				open: number;
				in_progress: number;
				done: number;
				cancelled: number;
			};

			activity.push({
				date: dateStr,
				open: row.open,
				inProgress: row.in_progress,
				done: row.done,
				cancelled: row.cancelled,
			});
		}

		return { activity, days };
	}
}
