/**
 * RepositoryService — CRUD operations for repositories.
 * Operates directly on the raw better-sqlite3 client for synchronous queries.
 */

import type Database from 'better-sqlite3';
import type { Repository, NewRepository, IndexingJob, NewIndexingJob } from '$lib/types';
import { resolveGitHubId, resolveLocalId } from '$lib/server/utils/id-resolver';
import {
	AlreadyExistsError,
	InvalidInputError,
	InvalidUrlError,
	NotFoundError
} from '$lib/server/utils/validation';

export interface AddRepositoryInput {
	source: 'github' | 'local';
	sourceUrl: string;
	title?: string;
	description?: string;
	branch?: string;
	githubToken?: string;
}

export interface UpdateRepositoryInput {
	title?: string;
	description?: string;
	branch?: string;
	githubToken?: string;
}

export interface RepositoryStats {
	totalSnippets: number;
	totalTokens: number;
	totalDocuments: number;
	lastIndexedAt: Date | null;
}

export class RepositoryService {
	constructor(private readonly db: Database.Database) {}

	/**
	 * List all repositories with optional filtering.
	 */
	list(options?: { state?: Repository['state']; limit?: number; offset?: number }): Repository[] {
		const limit = options?.limit ?? 50;
		const offset = options?.offset ?? 0;

		if (options?.state) {
			return this.db
				.prepare(
					`SELECT * FROM repositories WHERE state = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
				)
				.all(options.state, limit, offset) as Repository[];
		}

		return this.db
			.prepare(`SELECT * FROM repositories ORDER BY created_at DESC LIMIT ? OFFSET ?`)
			.all(limit, offset) as Repository[];
	}

	/**
	 * Count total repositories (optionally filtered by state).
	 */
	count(state?: Repository['state']): number {
		if (state) {
			const row = this.db
				.prepare(`SELECT COUNT(*) as n FROM repositories WHERE state = ?`)
				.get(state) as { n: number };
			return row.n;
		}
		const row = this.db
			.prepare(`SELECT COUNT(*) as n FROM repositories`)
			.get() as { n: number };
		return row.n;
	}

	/**
	 * Get a single repository by ID.
	 */
	get(id: string): Repository | null {
		return (
			(this.db.prepare(`SELECT * FROM repositories WHERE id = ?`).get(id) as Repository | undefined) ??
			null
		);
	}

	/**
	 * Add a new repository. Generates the canonical ID and queues an indexing job.
	 */
	add(input: AddRepositoryInput): Repository {
		// Validate required fields
		if (!input.sourceUrl?.trim()) {
			throw new InvalidInputError('sourceUrl is required', [
				{ field: 'sourceUrl', message: 'sourceUrl is required' }
			]);
		}

		// Derive canonical ID
		let id: string;
		let title: string;

		if (input.source === 'github') {
			try {
				id = resolveGitHubId(input.sourceUrl);
			} catch {
				throw new InvalidUrlError(
					`Invalid GitHub URL: ${input.sourceUrl}. Expected format: https://github.com/owner/repo`
				);
			}
			// Default title from owner/repo
			const parts = id.split('/').filter(Boolean);
			title = input.title ?? (parts[1] ?? id);
		} else {
			// local
			const existing = this.list({ limit: 9999 }).map((r) => r.id);
			id = resolveLocalId(input.sourceUrl, existing);
			const parts = input.sourceUrl.split('/');
			title = input.title ?? (parts.at(-1) ?? 'local-repo');
		}

		// Check for collision
		const existing = this.get(id);
		if (existing) {
			throw new AlreadyExistsError(`Repository ${id} already exists`);
		}

		const now = Math.floor(Date.now() / 1000);
		const repo: Record<string, unknown> = {
			id,
			title,
			description: input.description ?? null,
			source: input.source,
			source_url: input.sourceUrl,
			branch: input.branch ?? 'main',
			state: 'pending',
			total_snippets: 0,
			total_tokens: 0,
			trust_score: 0,
			benchmark_score: 0,
			stars: null,
			github_token: input.githubToken ?? null,
			last_indexed_at: null,
			created_at: now,
			updated_at: now
		};

		this.db
			.prepare(
				`INSERT INTO repositories
          (id, title, description, source, source_url, branch, state,
           total_snippets, total_tokens, trust_score, benchmark_score,
           stars, github_token, last_indexed_at, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
			)
			.run(
				repo.id,
				repo.title,
				repo.description,
				repo.source,
				repo.source_url,
				repo.branch,
				repo.state,
				repo.total_snippets,
				repo.total_tokens,
				repo.trust_score,
				repo.benchmark_score,
				repo.stars,
				repo.github_token,
				repo.last_indexed_at,
				repo.created_at,
				repo.updated_at
			);

		return this.get(id)!;
	}

	/**
	 * Update repository metadata.
	 */
	update(id: string, input: UpdateRepositoryInput): Repository {
		const existing = this.get(id);
		if (!existing) throw new NotFoundError(`Repository ${id} not found`);

		const now = Math.floor(Date.now() / 1000);
		const updates: string[] = [];
		const values: unknown[] = [];

		if (input.title !== undefined) {
			updates.push('title = ?');
			values.push(input.title);
		}
		if (input.description !== undefined) {
			updates.push('description = ?');
			values.push(input.description);
		}
		if (input.branch !== undefined) {
			updates.push('branch = ?');
			values.push(input.branch);
		}
		if (input.githubToken !== undefined) {
			updates.push('github_token = ?');
			values.push(input.githubToken);
		}

		if (updates.length === 0) return existing;

		updates.push('updated_at = ?');
		values.push(now);
		values.push(id);

		this.db.prepare(`UPDATE repositories SET ${updates.join(', ')} WHERE id = ?`).run(...values);

		return this.get(id)!;
	}

	/**
	 * Delete a repository and all associated data (cascades via FK).
	 */
	remove(id: string): void {
		const existing = this.get(id);
		if (!existing) throw new NotFoundError(`Repository ${id} not found`);

		this.db.prepare(`DELETE FROM repositories WHERE id = ?`).run(id);
	}

	/**
	 * Get aggregate statistics for a repository.
	 */
	getStats(id: string): RepositoryStats {
		const existing = this.get(id);
		if (!existing) throw new NotFoundError(`Repository ${id} not found`);

		const snippetStats = this.db
			.prepare(
				`SELECT COUNT(*) as total_snippets, COALESCE(SUM(token_count), 0) as total_tokens
           FROM snippets WHERE repository_id = ?`
			)
			.get(id) as { total_snippets: number; total_tokens: number };

		const docStats = this.db
			.prepare(`SELECT COUNT(*) as total_documents FROM documents WHERE repository_id = ?`)
			.get(id) as { total_documents: number };

		return {
			totalSnippets: snippetStats.total_snippets,
			totalTokens: snippetStats.total_tokens,
			totalDocuments: docStats.total_documents,
			lastIndexedAt: existing.lastIndexedAt
		};
	}

	/**
	 * Get all versions for a repository.
	 */
	getVersions(repositoryId: string): string[] {
		const rows = this.db
			.prepare(
				`SELECT tag FROM repository_versions WHERE repository_id = ? ORDER BY created_at DESC`
			)
			.all(repositoryId) as { tag: string }[];
		return rows.map((r) => r.tag);
	}

	/**
	 * Create an indexing job for a repository.
	 * If a job is already running, returns the existing job.
	 */
	createIndexingJob(repositoryId: string, versionId?: string): IndexingJob {
		// Check for running job
		const runningJob = this.db
			.prepare(
				`SELECT * FROM indexing_jobs
           WHERE repository_id = ? AND status IN ('queued', 'running')
           ORDER BY created_at DESC LIMIT 1`
			)
			.get(repositoryId) as IndexingJob | undefined;

		if (runningJob) return runningJob;

		const now = Math.floor(Date.now() / 1000);
		const job: Record<string, unknown> = {
			id: crypto.randomUUID(),
			repository_id: repositoryId,
			version_id: versionId ?? null,
			status: 'queued',
			progress: 0,
			total_files: 0,
			processed_files: 0,
			error: null,
			started_at: null,
			completed_at: null,
			created_at: now
		};

		this.db
			.prepare(
				`INSERT INTO indexing_jobs
          (id, repository_id, version_id, status, progress, total_files,
           processed_files, error, started_at, completed_at, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
			)
			.run(
				job.id,
				job.repository_id,
				job.version_id,
				job.status,
				job.progress,
				job.total_files,
				job.processed_files,
				job.error,
				job.started_at,
				job.completed_at,
				job.created_at
			);

		return this.db
			.prepare(`SELECT * FROM indexing_jobs WHERE id = ?`)
			.get(job.id) as IndexingJob;
	}
}
