package docs

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"time"
)

type Repository struct {
	db *sql.DB
}

type DocumentRecord struct {
	ID          string     `json:"id"`
	Path        string     `json:"path"`
	CurrentHash string     `json:"currentHash"`
	Title       string     `json:"title"`
	Tags        []string   `json:"tags"`
	CreatedAt   time.Time  `json:"createdAt"`
	UpdatedAt   time.Time  `json:"updatedAt"`
	ArchivedAt  *time.Time `json:"archivedAt,omitempty"`
}

type UserRecord struct {
	ID          string    `json:"id"`
	Email       string    `json:"email"`
	DisplayName string    `json:"displayName"`
	Role        string    `json:"role"`
	CreatedAt   time.Time `json:"createdAt"`
	LastSeenAt  time.Time `json:"lastSeenAt,omitempty"`
}

type AttachmentRecord struct {
	Hash         string
	OriginalName string
	ContentType  string
	SizeBytes    int64
	CreatedAt    time.Time
	DocumentPath string
}

type PersistDocumentInput struct {
	ID           string
	Path         string
	Title        string
	Hash         string
	PreviousHash string
	Tags         []string
}

func NewRepository(db *sql.DB) *Repository {
	return &Repository{db: db}
}

func (r *Repository) PersistDocument(ctx context.Context, input PersistDocumentInput) error {
	now := time.Now().UTC()
	tagsJSON, err := json.Marshal(input.Tags)
	if err != nil {
		return fmt.Errorf("marshal tags: %w", err)
	}

	existing, err := r.GetDocumentByPath(ctx, input.Path)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return err
	}

	documentID := input.ID
	createdAt := now
	if existing != nil {
		documentID = existing.ID
		createdAt = existing.CreatedAt
	}

	if _, err := r.db.ExecContext(ctx, `
		INSERT INTO documents (id, path, current_hash, title, tags, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT(path) DO UPDATE SET
			current_hash = excluded.current_hash,
			title = excluded.title,
			tags = excluded.tags,
			updated_at = excluded.updated_at
	`, documentID, input.Path, input.Hash, input.Title, string(tagsJSON), createdAt.Format(time.RFC3339), now.Format(time.RFC3339)); err != nil {
		return fmt.Errorf("upsert document %s: %w", input.Path, err)
	}

	if existing != nil && existing.CurrentHash == input.Hash {
		return nil
	}

	versionID := documentID + ":" + input.Hash[:12]
	if _, err := r.db.ExecContext(ctx, `
		INSERT OR IGNORE INTO document_versions (id, document_id, hash, previous_hash, created_at, change_summary)
		VALUES (?, ?, ?, ?, ?, ?)
	`, versionID, documentID, input.Hash, input.PreviousHash, now.Format(time.RFC3339), "filesystem sync"); err != nil {
		return fmt.Errorf("insert document version %s: %w", input.Path, err)
	}

	return nil
}

func (r *Repository) GetDocumentByPath(ctx context.Context, path string) (*DocumentRecord, error) {
	return r.getDocumentByPath(ctx, path, false)
}

func (r *Repository) GetDocumentByPathIncludingArchived(ctx context.Context, path string) (*DocumentRecord, error) {
	return r.getDocumentByPath(ctx, path, true)
}

func (r *Repository) GetDocumentByID(ctx context.Context, id string) (*DocumentRecord, error) {
	var (
		record   DocumentRecord
		tagsJSON string
		created  string
		updated  string
		archived sql.NullString
	)
	err := r.db.QueryRowContext(ctx, `
		SELECT id, path, current_hash, title, tags, created_at, updated_at, archived_at
		FROM documents
		WHERE id = ? AND archived_at IS NULL
	`, id).Scan(&record.ID, &record.Path, &record.CurrentHash, &record.Title, &tagsJSON, &created, &updated, &archived)
	if err != nil {
		return nil, err
	}
	if err := parseDocumentTimes(&record, created, updated); err != nil {
		return nil, err
	}
	if archived.Valid && archived.String != "" {
		t, err := time.Parse(time.RFC3339, archived.String)
		if err != nil {
			return nil, fmt.Errorf("parse document archived_at: %w", err)
		}
		record.ArchivedAt = &t
	}
	if err := json.Unmarshal([]byte(tagsJSON), &record.Tags); err != nil {
		return nil, fmt.Errorf("unmarshal document tags: %w", err)
	}
	return &record, nil
}

func (r *Repository) getDocumentByPath(ctx context.Context, path string, includeArchived bool) (*DocumentRecord, error) {
	var (
		record   DocumentRecord
		tagsJSON string
		created  string
		updated  string
		archived sql.NullString
	)

	query := `
		SELECT id, path, current_hash, title, tags, created_at, updated_at, archived_at
		FROM documents
		WHERE path = ?
	`
	if !includeArchived {
		query += ` AND archived_at IS NULL`
	}

	err := r.db.QueryRowContext(ctx, query, path).Scan(&record.ID, &record.Path, &record.CurrentHash, &record.Title, &tagsJSON, &created, &updated, &archived)
	if err != nil {
		return nil, err
	}

	if err := parseDocumentTimes(&record, created, updated); err != nil {
		return nil, err
	}
	if archived.Valid && archived.String != "" {
		t, err := time.Parse(time.RFC3339, archived.String)
		if err != nil {
			return nil, fmt.Errorf("parse document archived_at: %w", err)
		}
		record.ArchivedAt = &t
	}
	if err := json.Unmarshal([]byte(tagsJSON), &record.Tags); err != nil {
		return nil, fmt.Errorf("unmarshal document tags: %w", err)
	}

	return &record, nil
}

func (r *Repository) ListDocuments(ctx context.Context) ([]DocumentRecord, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, path, current_hash, title, tags, created_at, updated_at, archived_at
		FROM documents
		WHERE archived_at IS NULL
		ORDER BY path ASC
	`)
	if err != nil {
		return nil, fmt.Errorf("list documents: %w", err)
	}
	defer rows.Close()

	return scanDocumentRows(rows)
}

func (r *Repository) ListArchivedDocuments(ctx context.Context) ([]DocumentRecord, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, path, current_hash, title, tags, created_at, updated_at, archived_at
		FROM documents
		WHERE archived_at IS NOT NULL
		ORDER BY archived_at DESC
	`)
	if err != nil {
		return nil, fmt.Errorf("list archived documents: %w", err)
	}
	defer rows.Close()

	return scanDocumentRows(rows)
}

func scanDocumentRows(rows *sql.Rows) ([]DocumentRecord, error) {
	var records []DocumentRecord
	for rows.Next() {
		var (
			record   DocumentRecord
			tagsJSON string
			created  string
			updated  string
			archived sql.NullString
		)
		if err := rows.Scan(&record.ID, &record.Path, &record.CurrentHash, &record.Title, &tagsJSON, &created, &updated, &archived); err != nil {
			return nil, fmt.Errorf("scan document: %w", err)
		}
		if err := parseDocumentTimes(&record, created, updated); err != nil {
			return nil, err
		}
		if archived.Valid && archived.String != "" {
			t, err := time.Parse(time.RFC3339, archived.String)
			if err != nil {
				return nil, fmt.Errorf("parse document archived_at: %w", err)
			}
			record.ArchivedAt = &t
		}
		if err := json.Unmarshal([]byte(tagsJSON), &record.Tags); err != nil {
			return nil, fmt.Errorf("unmarshal document tags: %w", err)
		}
		records = append(records, record)
	}

	return records, rows.Err()
}

func (r *Repository) ArchiveDocument(ctx context.Context, path string) error {
	now := time.Now().UTC().Format(time.RFC3339)
	result, err := r.db.ExecContext(ctx, `
		UPDATE documents SET archived_at = ? WHERE path = ? AND archived_at IS NULL
	`, now, path)
	if err != nil {
		return fmt.Errorf("archive document %s: %w", path, err)
	}
	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("archive document rows affected %s: %w", path, err)
	}
	if n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

func (r *Repository) RestoreDocument(ctx context.Context, path string) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE documents SET archived_at = NULL WHERE path = ? AND archived_at IS NOT NULL
	`, path)
	if err != nil {
		return fmt.Errorf("restore document %s: %w", path, err)
	}
	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("restore document rows affected %s: %w", path, err)
	}
	if n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

func (r *Repository) ListUsers(ctx context.Context) ([]UserRecord, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, email, COALESCE(display_name, ''), COALESCE(role, 'viewer'), created_at, COALESCE(last_seen_at, '')
		FROM users
		ORDER BY created_at DESC
	`)
	if err != nil {
		return nil, fmt.Errorf("list users: %w", err)
	}
	defer rows.Close()

	var users []UserRecord
	for rows.Next() {
		var (
			user     UserRecord
			created  string
			lastSeen string
		)
		if err := rows.Scan(&user.ID, &user.Email, &user.DisplayName, &user.Role, &created, &lastSeen); err != nil {
			return nil, fmt.Errorf("scan user: %w", err)
		}
		createdAt, err := time.Parse(time.RFC3339, created)
		if err != nil {
			return nil, fmt.Errorf("parse user created_at: %w", err)
		}
		user.CreatedAt = createdAt
		if lastSeen != "" {
			parsed, err := time.Parse(time.RFC3339, lastSeen)
			if err != nil {
				return nil, fmt.Errorf("parse user last_seen_at: %w", err)
			}
			user.LastSeenAt = parsed
		}
		users = append(users, user)
	}

	return users, rows.Err()
}

func (r *Repository) UpsertUser(ctx context.Context, user UserRecord) (UserRecord, error) {
	now := time.Now().UTC()
	if user.ID == "" {
		user.ID = "user:" + user.Email
	}
	if user.Role == "" {
		user.Role = "viewer"
	}
	if user.DisplayName == "" {
		user.DisplayName = user.Email
	}
	user.CreatedAt = now
	user.LastSeenAt = now

	if _, err := r.db.ExecContext(ctx, `
		INSERT INTO users (id, email, display_name, role, created_at, last_seen_at)
		VALUES (?, ?, ?, ?, ?, ?)
		ON CONFLICT(email) DO UPDATE SET
			display_name = excluded.display_name,
			role = excluded.role,
			last_seen_at = excluded.last_seen_at
	`, user.ID, user.Email, user.DisplayName, user.Role, now.Format(time.RFC3339), now.Format(time.RFC3339)); err != nil {
		return UserRecord{}, fmt.Errorf("upsert user %s: %w", user.Email, err)
	}

	users, err := r.ListUsers(ctx)
	if err != nil {
		return UserRecord{}, err
	}
	for _, candidate := range users {
		if candidate.Email == user.Email {
			return candidate, nil
		}
	}
	return UserRecord{}, sql.ErrNoRows
}

func (r *Repository) SaveAttachment(ctx context.Context, record AttachmentRecord) error {
	if _, err := r.db.ExecContext(ctx, `
		INSERT INTO attachments (hash, original_name, content_type, size_bytes, created_at, document_path)
		VALUES (?, ?, ?, ?, ?, ?)
		ON CONFLICT(hash) DO UPDATE SET
			original_name = excluded.original_name,
			content_type = excluded.content_type,
			size_bytes = excluded.size_bytes,
			document_path = excluded.document_path
	`, record.Hash, record.OriginalName, record.ContentType, record.SizeBytes, record.CreatedAt.Format(time.RFC3339), record.DocumentPath); err != nil {
		return fmt.Errorf("save attachment: %w", err)
	}
	return nil
}

func (r *Repository) GetAttachment(ctx context.Context, hash string) (*AttachmentRecord, error) {
	var (
		record  AttachmentRecord
		created string
	)
	err := r.db.QueryRowContext(ctx, `
		SELECT hash, original_name, content_type, size_bytes, created_at, COALESCE(document_path, '')
		FROM attachments
		WHERE hash = ?
	`, hash).Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created, &record.DocumentPath)
	if err != nil {
		return nil, err
	}
	parsed, err := time.Parse(time.RFC3339, created)
	if err != nil {
		return nil, fmt.Errorf("parse attachment created_at: %w", err)
	}
	record.CreatedAt = parsed
	return &record, nil
}

func (r *Repository) ListAttachments(ctx context.Context) ([]AttachmentRecord, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT hash, original_name, content_type, size_bytes, created_at, COALESCE(document_path, '')
		FROM attachments
		ORDER BY created_at DESC
	`)
	if err != nil {
		return nil, fmt.Errorf("list attachments: %w", err)
	}
	defer rows.Close()

	var attachments []AttachmentRecord
	for rows.Next() {
		var record AttachmentRecord
		var created string
		if err := rows.Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created, &record.DocumentPath); err != nil {
			return nil, fmt.Errorf("scan attachment: %w", err)
		}
		record.CreatedAt, err = time.Parse(time.RFC3339, created)
		if err != nil {
			return nil, fmt.Errorf("parse attachment created_at: %w", err)
		}
		attachments = append(attachments, record)
	}
	return attachments, rows.Err()
}

type SearchResult struct {
	Path  string `json:"path"`
	Title string `json:"title"`
}

func (r *Repository) SearchDocuments(ctx context.Context, query string) ([]SearchResult, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT d.path, d.title
		FROM document_search ds
		JOIN documents d ON d.rowid = ds.rowid
		WHERE document_search MATCH ? AND d.archived_at IS NULL
		ORDER BY rank
		LIMIT 50
	`, query)
	if err != nil {
		return nil, fmt.Errorf("search documents: %w", err)
	}
	defer rows.Close()

	var results []SearchResult
	for rows.Next() {
		var result SearchResult
		if err := rows.Scan(&result.Path, &result.Title); err != nil {
			return nil, fmt.Errorf("scan search result: %w", err)
		}
		results = append(results, result)
	}

	return results, rows.Err()
}

func (r *Repository) SearchDocumentsByTag(ctx context.Context, tag string) ([]SearchResult, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT d.path, d.title
		FROM documents d
		WHERE EXISTS (
			SELECT 1 FROM json_each(d.tags) WHERE json_each.value = ?
		) AND d.archived_at IS NULL
		ORDER BY d.path
	`, tag)
	if err != nil {
		return nil, fmt.Errorf("search documents by tag: %w", err)
	}
	defer rows.Close()

	var results []SearchResult
	for rows.Next() {
		var result SearchResult
		if err := rows.Scan(&result.Path, &result.Title); err != nil {
			return nil, fmt.Errorf("scan tag search result: %w", err)
		}
		results = append(results, result)
	}

	return results, rows.Err()
}

func (r *Repository) UpdateSearchContent(ctx context.Context, path string, content string) error {
	if _, err := r.db.ExecContext(ctx, `
		UPDATE document_search SET content = ?
		WHERE rowid = (SELECT rowid FROM documents WHERE path = ?)
	`, content, path); err != nil {
		return fmt.Errorf("update search content: %w", err)
	}
	return nil
}

func (r *Repository) Ping(ctx context.Context) error {
	return r.db.PingContext(ctx)
}

func parseDocumentTimes(record *DocumentRecord, created string, updated string) error {
	createdAt, err := time.Parse(time.RFC3339, created)
	if err != nil {
		return fmt.Errorf("parse document created_at: %w", err)
	}
	updatedAt, err := time.Parse(time.RFC3339, updated)
	if err != nil {
		return fmt.Errorf("parse document updated_at: %w", err)
	}
	record.CreatedAt = createdAt
	record.UpdatedAt = updatedAt
	return nil
}
