package models

import (
	"database/sql"
	"errors"
	"fmt"
	"html/template"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/gomarkdown/markdown"
	"github.com/gomarkdown/markdown/html"
	"github.com/gomarkdown/markdown/parser"
)

type Post struct {
	ID          int
	Title       string
	Body        template.HTML
	Tags        TagList
	Created     time.Time
	Modified    time.Time
	IsDraft     bool
	URL         string
	Description string
	HeaderImage Image
}

type Image struct {
	Location string
	Mime     string
}

type PostModel struct {
	DB *sql.DB
}

func (m *PostModel) parsePostRow(s Scanner, parse bool) (Post, error) {
	var p Post
	var imgPath string

	err := s.Scan(&p.ID, &p.Title, &p.Body, &p.Created, &p.Modified, &p.IsDraft, &p.URL, &imgPath, &p.Description)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Post{}, ErrNoRecord
		} else {
			return Post{}, err
		}
	}

	tags, err := m.AllTagsForPost(p.ID)
	if err != nil {
		return Post{}, err
	}

	p.Tags = tags

	var mime string
	img, err := os.ReadFile("." + imgPath)
	// We have a default to fall back on, so we try to do that instead of
	// returning an error
	if err != nil {
		imgPath = "/static/img/bad_placeholder.png"
		mime = "image/png"
	} else {
		mime = http.DetectContentType(img)
	}

	p.HeaderImage = Image{
		Location: imgPath,
		Mime:     mime,
	}

	if parse {
		p.ParseBody()
	}

	return p, nil
}

func (p *Post) ParseBody() {
	body := []byte(p.Body)
	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
	pr := parser.NewWithExtensions(extensions)
	doc := pr.Parse(body)

	htmlFlags := html.CommonFlags
	opts := html.RendererOptions{Flags: htmlFlags}
	renderer := html.NewRenderer(opts)

	p.Body = template.HTML(markdown.Render(doc, renderer))
}

func (m *PostModel) Insert(p Post) (int, error) {
	stmt := "INSERT INTO post (title, body, is_draft, created, modified, url, header_image_path, meta_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"

	result, err := m.DB.Exec(stmt, p.Title, p.Body, p.IsDraft, p.Created, p.Modified, p.URL, p.HeaderImage.Location, p.Description)
	if err != nil {
		var mySQLError *mysql.MySQLError
		if errors.As(err, &mySQLError) {
			if mySQLError.Number == 1062 && strings.Contains(mySQLError.Message, "post_uc_url") {
				return 0, ErrDuplicateUrl
			}
		}
		return 0, err
	}

	id, err := result.LastInsertId()
	if err != nil {
		return 0, err
	}

	p.ID = int(id)
	err = m.UpdateTags(p)
	if err != nil {
		return p.ID, err
	}

	return p.ID, nil
}

func (m *PostModel) Update(p Post) error {
	stmt := "UPDATE post SET title=?, body=?, is_draft=?, url=?, header_image_path=?, meta_description=?, created=?, modified=? WHERE id=?"

	result, err := m.DB.Exec(stmt, p.Title, p.Body, p.IsDraft, p.URL, p.HeaderImage.Location, p.Description, p.Created, p.Modified, p.ID)
	if err != nil {
		var mySQLError *mysql.MySQLError
		if errors.As(err, &mySQLError) {
			if mySQLError.Number == 1062 && strings.Contains(mySQLError.Message, "post_uc_url") {
				return ErrDuplicateUrl
			}
		}
		return err
	}

	_, err = result.RowsAffected()
	if err != nil {
		return err
	}

	err = m.UpdateTags(p)
	if err != nil {
		return err
	}

	return nil
}

func (m *PostModel) UpdateTags(p Post) error {
	oldTags, err := m.AllTagsForPost(p.ID)
	if err != nil {
		return err
	}

	// (t)o (b)e (r)emoved
	var tbr TagList
	for _, t := range oldTags {
		if !p.Tags.Contains(t) {
			tbr = append(tbr, t)
		}
	}

	for _, t := range tbr {
		r, err := m.DeletePostTag(p.ID, t.ID)
		if err != nil {
			return err
		}

		if r != 1 {
			return errors.New(fmt.Sprintf("Unexpected number of deleted rows: %d", r))
		}
	}

	for _, t := range p.Tags {
		tid, err := m.InsertTagIfNotExists(t)
		if err != nil {
			return err
		}

		t.ID = tid

		err = m.InsertPostTagIfNotExists(p, t)
		if err != nil {
			return err
		}
	}

	return nil
}

func (m *PostModel) TogglePublishStatus(id int) error {
	stmt := "UPDATE post SET is_draft = NOT is_draft WHERE id = ?"

	result, err := m.DB.Exec(stmt, id)
	if err != nil {
		return err
	}

	_, err = result.RowsAffected()
	if err != nil {
		return err
	}

	return nil
}

func (m *PostModel) Get(id int) (Post, error) {
	stmt := `SELECT id, title, body, created, modified, is_draft, url, header_image_path, meta_description FROM post
	WHERE id=?`

	row := m.DB.QueryRow(stmt, id)
	return m.parsePostRow(row, false)
}

func (m *PostModel) GetByURL(url string) (Post, error) {
	stmt := `SELECT id, title, body, created, modified, is_draft, url, header_image_path, meta_description FROM post
	WHERE url=?`

	row := m.DB.QueryRow(stmt, url)
	return m.parsePostRow(row, false)
}

func (m *PostModel) Latest(n int) ([]Post, error) {
	stmt := `SELECT id, title, body, created, modified, is_draft, url, header_image_path, meta_description FROM post
	WHERE is_draft = FALSE ORDER BY created DESC LIMIT ?`

	rows, err := m.DB.Query(stmt, n)
	if err != nil {
		return nil, err
	}

	defer rows.Close()

	var posts []Post

	for rows.Next() {
		p, err := m.parsePostRow(rows, true)
		if err != nil {
			return nil, err
		}

		posts = append(posts, p)
	}

	if err = rows.Err(); err != nil {
		return nil, err
	}

	return posts, nil
}

func (m *PostModel) Filter(isDraft bool, tagName string) ([]Post, error) {
	posts, err := m.All()
	if err != nil {
		return nil, err
	}

	t := Tag{Name: tagName}
	fPosts := []Post{}
	for _, p := range posts {
		if p.Tags.Contains(t) {
			fPosts = append(fPosts, p)
		}
	}

	return fPosts, nil
}

func (m *PostModel) All() ([]Post, error) {
	stmt := `SELECT id, title, body, created, modified, is_draft, url, header_image_path, meta_description FROM post
	WHERE is_draft = FALSE ORDER BY created DESC`

	rows, err := m.DB.Query(stmt)
	if err != nil {
		return nil, err
	}

	defer rows.Close()

	var posts []Post

	for rows.Next() {
		p, err := m.parsePostRow(rows, true)
		if err != nil {
			return nil, err
		}

		posts = append(posts, p)
	}

	if err = rows.Err(); err != nil {
		return nil, err
	}

	return posts, nil
}

func (m *PostModel) PublishDraft(id int) (int, error) {
	stmt := "UPDATE post SET is_draft = TRUE WHERE id = ?"

	result, err := m.DB.Exec(stmt, id)
	if err != nil {
		return 0, err
	}

	postId, err := result.LastInsertId()
	if err != nil {
		return 0, err
	}

	return int(postId), nil
}

func (m *PostModel) AllDrafts() ([]Post, error) {
	stmt := `SELECT id, title, body, created, modified, is_draft, url, header_image_path, meta_description FROM post
       WHERE is_draft = TRUE ORDER BY id DESC`

	rows, err := m.DB.Query(stmt)
	if err != nil {
		return nil, err
	}

	defer rows.Close()

	var drafts []Post

	for rows.Next() {
		p, err := m.parsePostRow(rows, true)
		if err != nil {
			return nil, err
		}

		drafts = append(drafts, p)
	}

	if err = rows.Err(); err != nil {
		return nil, err
	}

	return drafts, nil
}