package postimport

import (
	"bytes"
	"crypto/rand"
	"encoding/csv"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
	"unicode"

	"gopkg.in/yaml.v3"
)

const defaultPostsDir = "content/posts"

type Options struct {
	CSVPath    string
	PostsDir   string
	Overwrite  bool
	WorkingDir string
}

type Result struct {
	Imported        int
	SkippedExisting int
	SkippedNonPost  int
}

type PostFile struct {
	Path        string
	Frontmatter PostFrontmatter
	Body        string
}

type PostFrontmatter struct {
	ID          string   `yaml:"id"`
	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"`
}

type csvArticle struct {
	ID          string
	Slug        string
	Title       string
	BodyMD      string
	BodyHTML    string
	Status      string
	ArchiveID   string
	AuthorID    string
	PublishedAt string
	CreatedAt   string
	UpdatedAt   string
	Type        string
}

func Import(options Options) (Result, error) {
	root, err := findProjectRoot(options.WorkingDir)
	if err != nil {
		return Result{}, err
	}

	csvPath := resolveRootPath(root, firstNonEmpty(options.CSVPath, "articles.csv"))
	postsDir := resolveRootPath(root, firstNonEmpty(options.PostsDir, defaultPostsDir))
	if err := os.MkdirAll(postsDir, 0o755); err != nil {
		return Result{}, err
	}

	file, err := os.Open(csvPath)
	if err != nil {
		return Result{}, err
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.FieldsPerRecord = -1
	reader.LazyQuotes = false

	headers, err := reader.Read()
	if err != nil {
		return Result{}, err
	}
	index := map[string]int{}
	for i, header := range headers {
		index[strings.TrimSpace(header)] = i
	}

	var result Result
	for rowNum := 2; ; rowNum++ {
		record, err := reader.Read()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return result, fmt.Errorf("%s row %d: %w", csvPath, rowNum, err)
		}

		article := csvArticle{
			ID:          csvValue(record, index, "id"),
			Slug:        csvValue(record, index, "slug"),
			Title:       csvValue(record, index, "title"),
			BodyMD:      csvValue(record, index, "body_md"),
			BodyHTML:    csvValue(record, index, "body_html"),
			Status:      csvValue(record, index, "status"),
			ArchiveID:   csvValue(record, index, "archive_id"),
			AuthorID:    csvValue(record, index, "author_id"),
			PublishedAt: csvValue(record, index, "published_at"),
			CreatedAt:   csvValue(record, index, "created_at"),
			UpdatedAt:   csvValue(record, index, "updated_at"),
			Type:        csvValue(record, index, "type"),
		}

		if strings.TrimSpace(article.Type) != "" && strings.TrimSpace(article.Type) != "post" {
			result.SkippedNonPost++
			continue
		}

		post, skippedExisting, err := articleToPost(root, postsDir, article, options.Overwrite)
		if err != nil {
			return result, fmt.Errorf("%s row %d (%s): %w", csvPath, rowNum, article.Slug, err)
		}
		if skippedExisting {
			result.SkippedExisting++
			continue
		}
		if err := writePostFile(post); err != nil {
			return result, fmt.Errorf("%s row %d (%s): %w", csvPath, rowNum, article.Slug, err)
		}
		result.Imported++
	}

	return result, nil
}

func articleToPost(root string, postsDir string, article csvArticle, overwrite bool) (PostFile, bool, error) {
	id := strings.TrimSpace(article.ID)
	if id == "" {
		id = randomID()
	}

	title := strings.TrimSpace(article.Title)
	if title == "" {
		return PostFile{}, false, errors.New("missing title")
	}

	slug := sanitizeSlug(article.Slug)
	if slug == "" {
		slug = fallbackSlug(title)
	}
	if slug == "" {
		return PostFile{}, false, errors.New("missing slug")
	}

	status := strings.TrimSpace(article.Status)
	if status != "published" && status != "draft" {
		status = "draft"
	}

	createdAt, err := normalizeLegacyTime(article.CreatedAt)
	if err != nil {
		return PostFile{}, false, fmt.Errorf("invalid created_at: %w", err)
	}
	updatedAt, err := normalizeLegacyTime(article.UpdatedAt)
	if err != nil {
		return PostFile{}, false, fmt.Errorf("invalid updated_at: %w", err)
	}

	var publishedAt *string
	if strings.TrimSpace(article.PublishedAt) != "" {
		normalized, err := normalizeLegacyTime(article.PublishedAt)
		if err != nil {
			return PostFile{}, false, fmt.Errorf("invalid published_at: %w", err)
		}
		publishedAt = &normalized
	}

	path := filepath.Join(postsDir, slug+".md")
	if _, err := os.Stat(path); err == nil {
		if !overwrite {
			return PostFile{}, true, nil
		}
	} else if !errors.Is(err, os.ErrNotExist) {
		return PostFile{}, false, err
	}

	finalSlug := slug
	if !overwrite {
		finalSlug, err = uniqueSlug(root, slug)
		if err != nil {
			return PostFile{}, false, err
		}
		path, err = uniquePostPath(postsDir, finalSlug)
		if err != nil {
			return PostFile{}, false, err
		}
	}

	body := strings.TrimLeft(article.BodyMD, "\n")
	if strings.TrimSpace(body) == "" {
		body = strings.TrimLeft(article.BodyHTML, "\n")
	}

	return PostFile{
		Path: path,
		Frontmatter: PostFrontmatter{
			ID:          id,
			Slug:        finalSlug,
			Title:       title,
			Summary:     "",
			Status:      status,
			Tags:        []string{},
			Cover:       "",
			Version:     1,
			SlugSource:  "manual",
			SlugLocked:  true,
			PublishedAt: publishedAt,
			CreatedAt:   createdAt,
			UpdatedAt:   updatedAt,
		},
		Body: body,
	}, false, nil
}

func csvValue(record []string, index map[string]int, key string) string {
	i, ok := index[key]
	if !ok || i >= len(record) {
		return ""
	}
	return record[i]
}

func normalizeLegacyTime(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", errors.New("empty time")
	}
	layouts := []string{
		"2006-01-02 15:04:05.999999999Z07",
		"2006-01-02 15:04:05.999999Z07",
		"2006-01-02 15:04:05Z07",
		time.RFC3339,
	}
	for _, layout := range layouts {
		parsed, err := time.Parse(layout, value)
		if err == nil {
			return parsed.Format(time.RFC3339), nil
		}
	}
	return "", fmt.Errorf("unsupported time format %q", value)
}

func writePostFile(post PostFile) error {
	var frontmatter bytes.Buffer
	encoder := yaml.NewEncoder(&frontmatter)
	encoder.SetIndent(2)
	if err := encoder.Encode(post.Frontmatter); err != nil {
		return err
	}
	if err := encoder.Close(); err != nil {
		return err
	}

	var output bytes.Buffer
	output.WriteString("---\n")
	output.Write(frontmatter.Bytes())
	output.WriteString("---\n\n")
	output.WriteString(strings.TrimLeft(post.Body, "\n"))

	tmp := post.Path + ".tmp"
	if err := os.WriteFile(tmp, output.Bytes(), 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, post.Path)
}

func loadPosts(root string, postsDir string) ([]PostFile, error) {
	dir := resolveRootPath(root, postsDir)
	entries, err := os.ReadDir(dir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	var posts []PostFile
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
			continue
		}
		posts = append(posts, PostFile{Path: filepath.Join(dir, entry.Name()), Frontmatter: PostFrontmatter{Slug: strings.TrimSuffix(entry.Name(), ".md")}})
	}
	return posts, nil
}

func slugExists(root string, slug string) bool {
	posts, err := loadPosts(root, defaultPostsDir)
	if err == nil {
		for _, post := range posts {
			fileSlug := strings.TrimSuffix(filepath.Base(post.Path), ".md")
			if post.Frontmatter.Slug == slug || fileSlug == slug {
				return true
			}
		}
	}

	_, err = os.Stat(filepath.Join(root, defaultPostsDir, slug+".md"))
	return err == nil
}

func uniqueSlug(root string, slug string) (string, error) {
	base := sanitizeSlug(slug)
	if base == "" {
		return "", errors.New("empty slug")
	}
	if !slugExists(root, base) {
		return base, nil
	}
	for i := 2; i < 1000; i++ {
		candidate := fmt.Sprintf("%s-%d", base, i)
		if !slugExists(root, candidate) {
			return candidate, nil
		}
	}
	return "", fmt.Errorf("could not find available slug for %q", base)
}

func uniquePostPath(postsDir string, slug string) (string, error) {
	candidate := filepath.Join(postsDir, slug+".md")
	if _, err := os.Stat(candidate); errors.Is(err, os.ErrNotExist) {
		return candidate, nil
	} else if err != nil {
		return "", err
	}
	for i := 2; i < 1000; i++ {
		candidate = filepath.Join(postsDir, fmt.Sprintf("%s-%d.md", slug, i))
		if _, err := os.Stat(candidate); errors.Is(err, os.ErrNotExist) {
			return candidate, nil
		} else if err != nil {
			return "", err
		}
	}
	return "", fmt.Errorf("could not find available filename for slug %q", slug)
}

func findProjectRoot(start string) (string, error) {
	wd := start
	if wd == "" {
		var err error
		wd, err = os.Getwd()
		if err != nil {
			return "", err
		}
	}
	for {
		if isProjectRoot(wd) {
			return wd, nil
		}
		parent := filepath.Dir(wd)
		if parent == wd {
			return "", errors.New("could not find project root")
		}
		wd = parent
	}
}

func isProjectRoot(dir string) bool {
	if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
		if _, err := os.Stat(filepath.Join(dir, "backend", "go.mod")); err == nil {
			return true
		}
	}
	if _, err := os.Stat(filepath.Join(dir, "backend", "cmd", "osaetctl")); err == nil {
		if _, err := os.Stat(filepath.Join(dir, "frontend", "site", "package.json")); err == nil {
			return true
		}
	}
	return false
}

func resolveRootPath(root string, path string) string {
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(root, path)
}

func fallbackSlug(title string) string {
	var words []string
	var b strings.Builder
	flush := func() {
		if b.Len() > 0 {
			words = append(words, b.String())
			b.Reset()
		}
	}
	for _, r := range strings.ToLower(title) {
		switch {
		case r >= 'a' && r <= 'z':
			b.WriteRune(r)
		case r >= '0' && r <= '9':
			b.WriteRune(r)
		case unicode.IsSpace(r) || r == '-' || r == '_' || r == '/':
			flush()
		default:
			flush()
		}
	}
	flush()
	if len(words) == 0 {
		return "post-" + time.Now().Format("20060102150405")
	}
	return sanitizeSlug(strings.Join(words, "-"))
}

func sanitizeSlug(slug string) string {
	slug = strings.ToLower(strings.TrimSpace(slug))
	re := regexp.MustCompile(`[^a-z0-9]+`)
	slug = re.ReplaceAllString(slug, "-")
	slug = strings.Trim(slug, "-")
	for strings.Contains(slug, "--") {
		slug = strings.ReplaceAll(slug, "--", "-")
	}
	if len(slug) > 80 {
		slug = strings.Trim(slug[:80], "-")
	}
	return slug
}

func randomID() string {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return fmt.Sprintf("post-%d", time.Now().UnixNano())
	}
	return hex.EncodeToString(b[:])
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}
