package cli

import (
	"database/sql"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

func runPostsImport(root string, args []string) error {
	fs := flag.NewFlagSet("posts import", flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	dbPathFlag := fs.String("db", defaultSQLitePath, "SQLite database path")
	if err := fs.Parse(args); err != nil {
		return err
	}

	dbPath := resolveRootPath(root, *dbPathFlag)
	if _, err := os.Stat(dbPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("database does not exist: %s; run `osaetctl db init` first", mustRel(root, dbPath))
		}
		return err
	}

	db, err := openSQLite(dbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	if err := applySQLiteSchema(db); err != nil {
		return err
	}

	posts, err := loadPosts(root)
	if err != nil {
		return err
	}

	imported := 0
	for _, post := range posts {
		if post.Frontmatter.ID == "" {
			return fmt.Errorf("%s: missing id", mustRel(root, post.Path))
		}
		if post.Frontmatter.Slug == "" {
			return fmt.Errorf("%s: missing slug", mustRel(root, post.Path))
		}
		if err := upsertSQLitePost(root, db, post); err != nil {
			return err
		}
		imported++
	}

	fmt.Printf("imported %d post(s) into %s\n", imported, mustRel(root, dbPath))
	return nil
}

func runPostsExport(root string, args []string) error {
	fs := flag.NewFlagSet("posts export", flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	dbPathFlag := fs.String("db", defaultSQLitePath, "SQLite database path")
	overwrite := fs.Bool("overwrite", false, "overwrite existing Markdown files")
	if err := fs.Parse(args); err != nil {
		return err
	}

	dbPath := resolveRootPath(root, *dbPathFlag)
	if _, err := os.Stat(dbPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("database does not exist: %s; run `osaetctl db init` first", mustRel(root, dbPath))
		}
		return err
	}

	db, err := openSQLite(dbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	posts, err := loadSQLitePosts(db)
	if err != nil {
		return err
	}

	exported, skipped, err := exportPostsToFilesCount(root, posts, *overwrite)
	if err != nil {
		return err
	}

	fmt.Printf("exported %d post(s), skipped %d existing file(s)\n", exported, skipped)
	return nil
}

func runPostsDiff(root string, args []string) error {
	fs := flag.NewFlagSet("posts diff", flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	dbPathFlag := fs.String("db", defaultSQLitePath, "SQLite database path")
	if err := fs.Parse(args); err != nil {
		return err
	}

	dbPath := resolveRootPath(root, *dbPathFlag)
	if _, err := os.Stat(dbPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("database does not exist: %s; run `osaetctl db init` first", mustRel(root, dbPath))
		}
		return err
	}

	db, err := openSQLite(dbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	return printPostsDiff(root, db)
}

func printPostsDiff(root string, db *sql.DB) error {
	filePosts, err := loadPosts(root)
	if err != nil {
		return err
	}
	dbPosts, err := loadSQLitePosts(db)
	if err != nil {
		return err
	}

	fileByID := map[string]postFile{}
	dbByID := map[string]postFile{}
	for _, post := range filePosts {
		if post.Frontmatter.ID == "" {
			return fmt.Errorf("%s: missing id", mustRel(root, post.Path))
		}
		fileByID[post.Frontmatter.ID] = post
	}
	for _, post := range dbPosts {
		dbByID[post.Frontmatter.ID] = post
	}

	ids := map[string]bool{}
	for id := range fileByID {
		ids[id] = true
	}
	for id := range dbByID {
		ids[id] = true
	}

	summary := map[string]int{}
	for _, id := range sortedBoolKeys(ids) {
		filePost, inFile := fileByID[id]
		dbPost, inDB := dbByID[id]
		switch {
		case inFile && !inDB:
			summary["only-file"]++
			fmt.Printf("only-file %-32s %s\n", filePost.Frontmatter.Slug, filePost.Frontmatter.Title)
		case !inFile && inDB:
			summary["only-db"]++
			fmt.Printf("only-db   %-32s %s\n", dbPost.Frontmatter.Slug, dbPost.Frontmatter.Title)
		default:
			if postsEquivalent(root, filePost, dbPost) {
				summary["same"]++
			} else {
				summary["changed"]++
				fmt.Printf("changed   %-32s %s\n", filePost.Frontmatter.Slug, filePost.Frontmatter.Title)
				printPostDiffFields(root, filePost, dbPost)
			}
		}
	}

	fmt.Printf("summary: same=%d changed=%d only-file=%d only-db=%d\n",
		summary["same"],
		summary["changed"],
		summary["only-file"],
		summary["only-db"],
	)
	return nil
}

func runPostsSync(root string, args []string) error {
	fs := flag.NewFlagSet("posts sync", flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	dbPathFlag := fs.String("db", defaultSQLitePath, "SQLite database path")
	from := fs.String("from", "", "sync source: files, db, or auto")
	yes := fs.Bool("yes", false, "confirm automatic sync without prompting")
	if err := fs.Parse(args); err != nil {
		return err
	}

	dbPath := resolveRootPath(root, *dbPathFlag)
	if _, err := os.Stat(dbPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("database does not exist: %s; run `osaetctl db init` first", mustRel(root, dbPath))
		}
		return err
	}

	db, err := openSQLite(dbPath)
	if err != nil {
		return err
	}
	defer db.Close()
	if err := applySQLiteSchema(db); err != nil {
		return err
	}

	switch *from {
	case "files":
		return syncFromFiles(root, db, dbPath)
	case "db":
		return syncFromDB(root, db)
	case "auto":
		return syncAuto(root, db)
	case "":
		if err := printPostsDiff(root, db); err != nil {
			return err
		}
		if !*yes && !confirm("Auto Sync by newer updated_at?") {
			fmt.Println("No changes applied.")
			fmt.Println("Use `osaetctl posts sync --from files` to write Markdown into SQLite.")
			fmt.Println("Use `osaetctl posts sync --from db` to write SQLite into Markdown.")
			return nil
		}
		return syncAuto(root, db)
	default:
		return errors.New("--from must be files, db, or auto")
	}
}

func syncFromFiles(root string, db *sql.DB, dbPath string) error {
	posts, err := loadPosts(root)
	if err != nil {
		return err
	}
	for _, post := range posts {
		if post.Frontmatter.ID == "" {
			return fmt.Errorf("%s: missing id", mustRel(root, post.Path))
		}
		if err := upsertSQLitePost(root, db, post); err != nil {
			return err
		}
	}
	fmt.Printf("synced %d file post(s) into %s\n", len(posts), mustRel(root, dbPath))
	return nil
}

func syncFromDB(root string, db *sql.DB) error {
	posts, err := loadSQLitePosts(db)
	if err != nil {
		return err
	}
	if err := exportPostsToFiles(root, posts, true); err != nil {
		return err
	}
	fmt.Printf("synced %d db post(s) into Markdown files\n", len(posts))
	return nil
}

func syncAuto(root string, db *sql.DB) error {
	filePosts, err := loadPosts(root)
	if err != nil {
		return err
	}
	dbPosts, err := loadSQLitePosts(db)
	if err != nil {
		return err
	}

	fileByID := map[string]postFile{}
	dbByID := map[string]postFile{}
	ids := map[string]bool{}
	for _, post := range filePosts {
		if post.Frontmatter.ID == "" {
			return fmt.Errorf("%s: missing id", mustRel(root, post.Path))
		}
		fileByID[post.Frontmatter.ID] = post
		ids[post.Frontmatter.ID] = true
	}
	for _, post := range dbPosts {
		dbByID[post.Frontmatter.ID] = post
		ids[post.Frontmatter.ID] = true
	}

	filesToDB := 0
	dbToFiles := 0
	for _, id := range sortedBoolKeys(ids) {
		filePost, inFile := fileByID[id]
		dbPost, inDB := dbByID[id]

		switch {
		case inFile && !inDB:
			if err := upsertSQLitePost(root, db, filePost); err != nil {
				return err
			}
			filesToDB++
		case !inFile && inDB:
			if err := exportPostsToFiles(root, []postFile{dbPost}, true); err != nil {
				return err
			}
			dbToFiles++
		case inFile && inDB:
			if postsEquivalent(root, filePost, dbPost) {
				continue
			}
			fileTime, fileOK := parseTime(filePost.Frontmatter.UpdatedAt)
			dbTime, dbOK := parseTime(dbPost.Frontmatter.UpdatedAt)
			if !fileOK && !dbOK {
				fmt.Printf("skipped %s: cannot compare updated_at\n", filePost.Frontmatter.Slug)
				continue
			}
			if fileOK && (!dbOK || fileTime.After(dbTime)) {
				if err := upsertSQLitePost(root, db, filePost); err != nil {
					return err
				}
				filesToDB++
			} else if err := exportPostsToFiles(root, []postFile{dbPost}, true); err != nil {
				return err
			} else {
				dbToFiles++
			}
		}
	}

	fmt.Printf("auto sync complete: files->db=%d db->files=%d\n", filesToDB, dbToFiles)
	return nil
}

func upsertSQLitePost(root string, db *sql.DB, post postFile) error {
	tagsJSON, err := json.Marshal(post.Frontmatter.Tags)
	if err != nil {
		return err
	}

	publishedAt := sql.NullString{}
	if post.Frontmatter.PublishedAt != nil && strings.TrimSpace(*post.Frontmatter.PublishedAt) != "" {
		publishedAt.Valid = true
		publishedAt.String = *post.Frontmatter.PublishedAt
	}

	_, err = db.Exec(`INSERT INTO posts (
		id,
		slug,
		title,
		summary,
		content_markdown,
		status,
		tags_json,
		cover,
		file_path,
		version,
		content_hash,
		slug_source,
		slug_locked,
		published_at,
		created_at,
		updated_at
	) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	ON CONFLICT(id) DO UPDATE SET
		slug = excluded.slug,
		title = excluded.title,
		summary = excluded.summary,
		content_markdown = excluded.content_markdown,
		status = excluded.status,
		tags_json = excluded.tags_json,
		cover = excluded.cover,
		file_path = excluded.file_path,
		version = excluded.version,
		content_hash = excluded.content_hash,
		slug_source = excluded.slug_source,
		slug_locked = excluded.slug_locked,
		published_at = excluded.published_at,
		created_at = excluded.created_at,
		updated_at = excluded.updated_at,
		deleted_at = NULL`,
		post.Frontmatter.ID,
		post.Frontmatter.Slug,
		post.Frontmatter.Title,
		post.Frontmatter.Summary,
		post.Body,
		post.Frontmatter.Status,
		string(tagsJSON),
		post.Frontmatter.Cover,
		mustRel(root, post.Path),
		post.Frontmatter.Version,
		contentHash(post.Body),
		post.Frontmatter.SlugSource,
		boolInt(post.Frontmatter.SlugLocked),
		publishedAt,
		post.Frontmatter.CreatedAt,
		post.Frontmatter.UpdatedAt,
	)
	return err
}

func loadSQLitePosts(db *sql.DB) ([]postFile, error) {
	rows, err := db.Query(`SELECT
		id,
		slug,
		title,
		summary,
		content_markdown,
		status,
		tags_json,
		cover,
		file_path,
		version,
		slug_source,
		slug_locked,
		published_at,
		created_at,
		updated_at
	FROM posts
	WHERE deleted_at IS NULL
	ORDER BY COALESCE(published_at, updated_at) DESC, title ASC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var posts []postFile
	for rows.Next() {
		var post postFile
		var tagsJSON string
		var publishedAt sql.NullString
		var slugLocked int
		if err := rows.Scan(
			&post.Frontmatter.ID,
			&post.Frontmatter.Slug,
			&post.Frontmatter.Title,
			&post.Frontmatter.Summary,
			&post.Body,
			&post.Frontmatter.Status,
			&tagsJSON,
			&post.Frontmatter.Cover,
			&post.Path,
			&post.Frontmatter.Version,
			&post.Frontmatter.SlugSource,
			&slugLocked,
			&publishedAt,
			&post.Frontmatter.CreatedAt,
			&post.Frontmatter.UpdatedAt,
		); err != nil {
			return nil, err
		}

		if err := json.Unmarshal([]byte(tagsJSON), &post.Frontmatter.Tags); err != nil {
			return nil, err
		}
		if publishedAt.Valid {
			post.Frontmatter.PublishedAt = &publishedAt.String
		}
		post.Frontmatter.SlugLocked = slugLocked != 0
		posts = append(posts, post)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return posts, nil
}

func exportPostsToFiles(root string, posts []postFile, overwrite bool) error {
	_, _, err := exportPostsToFilesCount(root, posts, overwrite)
	return err
}

func exportPostsToFilesCount(root string, posts []postFile, overwrite bool) (int, int, error) {
	postsDir := filepath.Join(root, defaultPostsDir)
	if err := os.MkdirAll(postsDir, 0o755); err != nil {
		return 0, 0, err
	}

	exported := 0
	skipped := 0
	for _, post := range posts {
		if post.Frontmatter.Slug == "" {
			return exported, skipped, fmt.Errorf("post %s has empty slug", post.Frontmatter.ID)
		}

		path := filepath.Join(postsDir, sanitizeSlug(post.Frontmatter.Slug)+".md")
		post.Path = path
		if _, err := os.Stat(path); err == nil && !overwrite {
			skipped++
			continue
		} else if err != nil && !errors.Is(err, os.ErrNotExist) {
			return exported, skipped, err
		}

		if err := writePostFile(post); err != nil {
			return exported, skipped, err
		}
		exported++
	}
	return exported, skipped, nil
}

func postsEquivalent(root string, filePost postFile, dbPost postFile) bool {
	return len(changedPostFields(root, filePost, dbPost)) == 0
}

func printPostDiffFields(root string, filePost postFile, dbPost postFile) {
	for _, field := range changedPostFields(root, filePost, dbPost) {
		fmt.Printf("  - %s\n", field)
	}
}

func changedPostFields(root string, filePost postFile, dbPost postFile) []string {
	var fields []string
	if filePost.Frontmatter.Slug != dbPost.Frontmatter.Slug {
		fields = append(fields, "slug")
	}
	if filePost.Frontmatter.Title != dbPost.Frontmatter.Title {
		fields = append(fields, "title")
	}
	if filePost.Frontmatter.Summary != dbPost.Frontmatter.Summary {
		fields = append(fields, "summary")
	}
	if filePost.Frontmatter.Status != dbPost.Frontmatter.Status {
		fields = append(fields, "status")
	}
	if strings.Join(filePost.Frontmatter.Tags, "\x00") != strings.Join(dbPost.Frontmatter.Tags, "\x00") {
		fields = append(fields, "tags")
	}
	if filePost.Frontmatter.Cover != dbPost.Frontmatter.Cover {
		fields = append(fields, "cover")
	}
	if filePost.Frontmatter.Version != dbPost.Frontmatter.Version {
		fields = append(fields, "version")
	}
	if filePost.Frontmatter.SlugSource != dbPost.Frontmatter.SlugSource {
		fields = append(fields, "slug_source")
	}
	if filePost.Frontmatter.SlugLocked != dbPost.Frontmatter.SlugLocked {
		fields = append(fields, "slug_locked")
	}
	if stringPtrValue(filePost.Frontmatter.PublishedAt) != stringPtrValue(dbPost.Frontmatter.PublishedAt) {
		fields = append(fields, "published_at")
	}
	if filePost.Frontmatter.CreatedAt != dbPost.Frontmatter.CreatedAt {
		fields = append(fields, "created_at")
	}
	if filePost.Frontmatter.UpdatedAt != dbPost.Frontmatter.UpdatedAt {
		fields = append(fields, "updated_at")
	}
	if contentHash(filePost.Body) != contentHash(dbPost.Body) {
		fields = append(fields, "content")
	}
	filePath := mustRel(root, filePost.Path)
	if dbPost.Path != "" && filePath != dbPost.Path {
		fields = append(fields, "file_path")
	}
	return fields
}
