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');
const DEFAULT_LIMIT = 100;
const SORT_FIELDS = {
    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 = {
    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) {
    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() {
    return `task_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function generateChangelogId() {
    return `taskch_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function toTask(row) {
    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) : 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) {
    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) {
    return {
        id: row.id,
        created_at: new Date(row.created_at).toISOString(),
        name: row.name,
        color: row.color ?? undefined,
    };
}
function generateCommentId() {
    return `comment_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function generateTagId() {
    return `tag_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function generateUserId() {
    return `usr_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function generateProjectId() {
    return `prj_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
}
function toChangelogEntry(row) {
    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 {
    #db;
    #projectPath;
    constructor(db, projectPath) {
        this.#db = db;
        this.#projectPath = projectPath;
    }
    async create(params) {
        const trimmedTitle = params?.title?.trim();
        if (!trimmedTitle) {
            throw new TaskTitleRequiredError();
        }
        const id = generateTaskId();
        const timestamp = now();
        const status = params.status ? normalizeTaskStatus(params.status) : 'open';
        const priority = 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 = {
            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) {
        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);
        if (!row) {
            return null;
        }
        return toTask(row);
    }
    async list(params) {
        const filters = ['project_path = ?'];
        const values = [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);
        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);
        return {
            tasks: rows.map(toTask),
            total: totalRow.count,
            limit,
            offset,
        };
    }
    async update(id, params) {
        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);
            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 = {
                ...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 = [];
            const compare = (field, oldValue, newValue) => {
                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) {
        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);
            if (!existing) {
                throw new TaskNotFoundError();
            }
            if (existing.status === 'done') {
                throw new TaskAlreadyClosedError();
            }
            const timestamp = now();
            const nowIso = new Date(timestamp).toISOString();
            const updated = {
                ...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, params) {
        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);
        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);
        return {
            changelog: rows.map(toChangelogEntry),
            total: totalRow.count,
            limit,
            offset,
        };
    }
    async softDelete(id) {
        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) {
        const conditions = ['project_path = ?', 'deleted = 0'];
        const args = [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);
        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) {
        const conditions = ['project_path = ?', 'deleted = 0'];
        const args = [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);
        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),
                    priority: (params.new_priority ?? r.priority),
                })),
                count: rows.length,
                dry_run: true,
            };
        }
        const timestamp = now();
        const ids = rows.map((r) => r.id);
        const placeholders = ids.map(() => '?').join(', ');
        const updateFields = ['updated_at = ?'];
        const updateArgs = [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),
                priority: (params.new_priority ?? r.priority),
            })),
            count: rows.length,
            dry_run: false,
        };
    }
    async batchClose(params) {
        // Resolve closer ID from either closed_id or closer entity ref
        const closerId = params.closed_id ?? params.closer?.id ?? null;
        const conditions = ['project_path = ?', 'deleted = 0', "status != 'done'"];
        const args = [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);
        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, body, userId) {
        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) {
        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);
        if (!row) {
            throw new CommentNotFoundError();
        }
        return toComment(row);
    }
    async updateComment(commentId, body) {
        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) {
        const stmt = this.#db.prepare(`
			DELETE FROM task_comment_storage
			WHERE project_path = ? AND id = ?
		`);
        stmt.run(this.#projectPath, commentId);
    }
    async listComments(taskId, params) {
        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);
        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);
        return {
            comments: rows.map(toComment),
            total: totalRow.count,
            limit,
            offset,
        };
    }
    async createTag(name, color) {
        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) {
        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);
        if (!row) {
            throw new TagNotFoundError();
        }
        return toTag(row);
    }
    async updateTag(tagId, name, color) {
        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) {
        // 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() {
        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);
        return {
            tags: rows.map(toTag),
        };
    }
    async addTagToTask(taskId, tagId) {
        // 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, tagId) {
        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) {
        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);
        return rows.map(toTag);
    }
    // Attachment methods — not supported in local storage
    async uploadAttachment(_taskId, _params) {
        throw new AttachmentNotSupportedError();
    }
    async confirmAttachment(_attachmentId) {
        throw new AttachmentNotSupportedError();
    }
    async downloadAttachment(_attachmentId) {
        throw new AttachmentNotSupportedError();
    }
    async listAttachments(_taskId) {
        throw new AttachmentNotSupportedError();
    }
    async deleteAttachment(_attachmentId) {
        throw new AttachmentNotSupportedError();
    }
    async listUsers() {
        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);
        return {
            users: rows.map((row) => ({
                id: row.id,
                name: row.name,
                type: row.type,
            })),
        };
    }
    async listProjects() {
        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);
        return {
            projects: rows.map((row) => ({
                id: row.id,
                name: row.name,
            })),
        };
    }
    async createUser(params) {
        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) {
        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);
        if (!row) {
            throw new UserNotFoundError();
        }
        return { id: row.id, name: row.name, type: row.type };
    }
    async deleteUser(userId) {
        const stmt = this.#db.prepare(`
			DELETE FROM task_user_storage
			WHERE project_path = ? AND id = ?
		`);
        stmt.run(this.#projectPath, userId);
    }
    async createProject(params) {
        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) {
        const query = this.#db.query(`
			SELECT id, name
			FROM task_project_storage
			WHERE project_path = ? AND id = ?
		`);
        const row = query.get(this.#projectPath, projectId);
        if (!row) {
            throw new ProjectNotFoundError();
        }
        return { id: row.id, name: row.name };
    }
    async deleteProject(projectId) {
        const stmt = this.#db.prepare(`
			DELETE FROM task_project_storage
			WHERE project_path = ? AND id = ?
		`);
        stmt.run(this.#projectPath, projectId);
    }
    async getActivity(params) {
        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);
            activity.push({
                date: dateStr,
                open: row.open,
                inProgress: row.in_progress,
                done: row.done,
                cancelled: row.cancelled,
            });
        }
        return { activity, days };
    }
}
//# sourceMappingURL=task.js.map