package admin

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
)

type MarkdownImportResult struct {
	Imported int
	Skipped  int
}

type markdownFrontmatter struct {
	Slug        string   `yaml:"slug"`
	Title       string   `yaml:"title"`
	Summary     string   `yaml:"summary"`
	Status      string   `yaml:"status"`
	Tags        []string `yaml:"tags"`
	Cover       string   `yaml:"cover"`
	Version     int      `yaml:"version"`
	SlugSource  string   `yaml:"slug_source"`
	SlugLocked  bool     `yaml:"slug_locked"`
	PublishedAt string   `yaml:"published_at"`
	CreatedAt   string   `yaml:"created_at"`
	UpdatedAt   string   `yaml:"updated_at"`
}

func (s *Store) ImportMarkdownPosts(ctx context.Context, postsDir string) (MarkdownImportResult, error) {
	entries, err := os.ReadDir(postsDir)
	if err != nil {
		return MarkdownImportResult{}, fmt.Errorf("read posts dir: %w", err)
	}

	var result MarkdownImportResult
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
			continue
		}

		post, err := readMarkdownPost(filepath.Join(postsDir, entry.Name()))
		if err != nil {
			return result, err
		}
		if post.Title == "" || post.Slug == "" {
			result.Skipped++
			continue
		}
		if err := s.upsertImportedPost(ctx, post); err != nil {
			return result, err
		}
		result.Imported++
	}

	return result, nil
}

func readMarkdownPost(path string) (Post, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return Post{}, fmt.Errorf("read markdown post %s: %w", path, err)
	}

	frontmatterData, body, err := splitMarkdownFrontmatter(data)
	if err != nil {
		return Post{}, fmt.Errorf("parse markdown post %s: %w", path, err)
	}

	var meta markdownFrontmatter
	if err := yaml.Unmarshal(frontmatterData, &meta); err != nil {
		return Post{}, fmt.Errorf("parse frontmatter %s: %w", path, err)
	}

	slug := strings.TrimSpace(meta.Slug)
	if slug == "" {
		slug = strings.TrimSuffix(filepath.Base(path), ".md")
	}
	status := PostStatus(strings.TrimSpace(meta.Status))
	if !ValidPostStatus(status) || status == PostStatusDeleted {
		status = PostStatusDraft
	}
	version := meta.Version
	if version < 1 {
		version = 1
	}
	slugSource := strings.TrimSpace(meta.SlugSource)
	if slugSource == "" {
		slugSource = "manual"
	}

	now := time.Now()
	createdAt := parseFrontmatterTime(meta.CreatedAt, now)
	updatedAt := parseFrontmatterTime(meta.UpdatedAt, createdAt)
	var publishedAt *time.Time
	if parsed, ok := parseOptionalFrontmatterTime(meta.PublishedAt); ok {
		publishedAt = &parsed
	}

	return Post{
		Slug:         slug,
		Title:        strings.TrimSpace(meta.Title),
		Summary:      meta.Summary,
		BodyMarkdown: strings.TrimLeft(string(body), "\n"),
		Status:       status,
		Tags:         normalizeTagNames(meta.Tags),
		Cover:        meta.Cover,
		Version:      version,
		SlugSource:   slugSource,
		SlugLocked:   meta.SlugLocked,
		PublishedAt:  publishedAt,
		CreatedAt:    createdAt,
		UpdatedAt:    updatedAt,
	}, nil
}

func (s *Store) upsertImportedPost(ctx context.Context, post Post) error {
	tx, err := s.db.Begin(ctx)
	if err != nil {
		return fmt.Errorf("begin markdown import: %w", err)
	}
	defer tx.Rollback(ctx)

	imported, err := scanPost(tx.QueryRow(ctx, `
INSERT INTO posts (
    slug, title, summary, body_markdown, status, cover, version,
    slug_source, slug_locked, published_at, created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (slug)
DO UPDATE SET
    title = excluded.title,
    summary = excluded.summary,
    body_markdown = excluded.body_markdown,
    status = excluded.status,
    cover = excluded.cover,
    version = GREATEST(posts.version, excluded.version),
    slug_source = excluded.slug_source,
    slug_locked = excluded.slug_locked,
    published_at = excluded.published_at,
    updated_at = excluded.updated_at,
    deleted_at = NULL
RETURNING id, slug, title, summary, body_markdown, status, cover, version,
          slug_source, slug_locked, published_at, created_at, updated_at, deleted_at`,
		post.Slug,
		post.Title,
		post.Summary,
		post.BodyMarkdown,
		post.Status,
		post.Cover,
		post.Version,
		post.SlugSource,
		post.SlugLocked,
		post.PublishedAt,
		post.CreatedAt,
		post.UpdatedAt,
	))
	if err != nil {
		return fmt.Errorf("upsert markdown post %s: %w", post.Slug, err)
	}
	if err := replacePostTags(ctx, tx, imported.ID, post.Tags); err != nil {
		return err
	}

	if _, err := tx.Exec(ctx, `
INSERT INTO post_versions (post_id, version, title, summary, body_markdown, status, reason)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (post_id, version) DO NOTHING`,
		imported.ID,
		imported.Version,
		imported.Title,
		imported.Summary,
		imported.BodyMarkdown,
		imported.Status,
		VersionReasonImport,
	); err != nil {
		return fmt.Errorf("insert markdown import version %s: %w", post.Slug, err)
	}

	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("commit markdown import: %w", err)
	}
	return nil
}

func splitMarkdownFrontmatter(data []byte) ([]byte, []byte, error) {
	if !bytes.HasPrefix(data, []byte("---\n")) {
		return nil, nil, errors.New("missing frontmatter opening marker")
	}

	rest := data[len("---\n"):]
	idx := bytes.Index(rest, []byte("\n---"))
	if idx < 0 {
		return nil, nil, errors.New("missing frontmatter closing marker")
	}

	frontmatter := rest[:idx]
	body := rest[idx+len("\n---"):]
	if bytes.HasPrefix(body, []byte("\r\n")) {
		body = body[2:]
	} else if bytes.HasPrefix(body, []byte("\n")) {
		body = body[1:]
	}
	return frontmatter, body, nil
}

func parseFrontmatterTime(value string, fallback time.Time) time.Time {
	if parsed, ok := parseOptionalFrontmatterTime(value); ok {
		return parsed
	}
	return fallback
}

func parseOptionalFrontmatterTime(value string) (time.Time, bool) {
	value = strings.TrimSpace(value)
	if value == "" {
		return time.Time{}, false
	}

	layouts := []string{
		time.RFC3339,
		"2006-01-02 15:04:05.999999999Z07",
		"2006-01-02 15:04:05.999999Z07",
		"2006-01-02 15:04:05Z07",
		"2006-01-02 15:04:05",
	}
	for _, layout := range layouts {
		if parsed, err := time.Parse(layout, value); err == nil {
			return parsed, true
		}
	}
	return time.Time{}, false
}
