package main

import (
	"database/sql"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gocolly/colly"
	"github.com/google/uuid"
	_ "github.com/mattn/go-sqlite3" // SQLite driver
)

// Book struct now includes boolean fields for read status
type Book struct {
	ID         int
	Name       string
	ImagePath  string
	TuomasRead bool
	JenniRead  bool
}

const dbFile = "books.db"
const imagesDir = "./images"

func main() {
	db, err := initDB()
	if err != nil {
		log.Fatal("Failed to initialize database:", err)
	}
	defer db.Close()

	ticker := time.NewTicker(6 * time.Hour)
	go func() {
		log.Println("Running initial crawl on startup...")
		crawl_dua(db)
	}()
	go func() {
		for t := range ticker.C {
			log.Printf("Running scheduled crawl at %v", t)
			crawl_dua(db)
		}
	}()

	startServer(db)
}

// initDB now safely adds the new columns if they don't already exist.
func initDB() (*sql.DB, error) {
	db, err := sql.Open("sqlite3", dbFile)
	if err != nil {
		return nil, err
	}

	createTableSQL := `CREATE TABLE IF NOT EXISTS books (
        "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        "name" TEXT NOT NULL UNIQUE,
        "image_path" TEXT
    );`

	if _, err = db.Exec(createTableSQL); err != nil {
		return nil, err
	}

	// --- NEW: Safely add new columns for read status ---
	addColumn(db, "books", "tuomas_read", "INTEGER DEFAULT 0")
	addColumn(db, "books", "jenni_read", "INTEGER DEFAULT 0")
	// --- END NEW ---

	log.Println("Database initialized successfully.")
	return db, nil
}

// --- NEW: Helper function to add a column only if it doesn't already exist. ---
func addColumn(db *sql.DB, tableName, columnName, columnType string) {
	rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
	if err != nil {
		log.Printf("Error checking table info for %s: %v", tableName, err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var cid, notnull, pk int
		var name, type_ string
		var dflt_value sql.NullString
		if err := rows.Scan(&cid, &name, &type_, &notnull, &dflt_value, &pk); err != nil {
			log.Printf("Error scanning table info row: %v", err)
			return
		}
		if name == columnName {
			return // Column already exists, do nothing.
		}
	}

	log.Printf("Column '%s' not found. Adding it to table '%s'.", columnName, tableName)
	_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", tableName, columnName, columnType))
	if err != nil {
		log.Printf("Failed to add column '%s' to table '%s': %v", columnName, tableName, err)
	} else {
		log.Printf("Successfully added column '%s'.", columnName)
	}
}

// --- END NEW ---

func startServer(db *sql.DB) {
	r := gin.Default()

	r.Static("/images", imagesDir)
	r.LoadHTMLGlob("templates/*.html")

	r.GET("/", func(c *gin.Context) {
		books, err := getAllBooks(db)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve books"})
			return
		}
		c.HTML(http.StatusOK, "books.html", gin.H{
			"books": books,
		})
	})

	// --- NEW: Endpoint to handle updating the read status ---
	r.POST("/books/:id/update", func(c *gin.Context) {
		idStr := c.Param("id")
		var payload struct {
			Person string `json:"person"`
			Read   bool   `json:"read"`
		}

		if err := c.ShouldBindJSON(&payload); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
			return
		}

		bookID, err := strconv.Atoi(idStr)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"})
			return
		}

		err = updateReadStatus(db, bookID, payload.Person, payload.Read)
		if err != nil {
			log.Printf("Error updating status for book %d: %v", bookID, err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status"})
			return
		}

		c.JSON(http.StatusOK, gin.H{"status": "success", "person": payload.Person, "read": payload.Read})
	})
	// --- END NEW ---

	log.Println("Starting server on http://localhost:8080/books")
	r.Run(":8080")
}

// --- NEW: Function to update the read status in the database ---
func updateReadStatus(db *sql.DB, bookID int, person string, read bool) error {
	var columnName string
	switch strings.ToLower(person) {
	case "tuomas":
		columnName = "tuomas_read"
	case "jenni":
		columnName = "jenni_read"
	default:
		return fmt.Errorf("invalid person specified: %s", person)
	}

	updateSQL := fmt.Sprintf("UPDATE books SET %s = ? WHERE id = ?", columnName)
	statement, err := db.Prepare(updateSQL)
	if err != nil {
		return err
	}
	defer statement.Close()

	_, err = statement.Exec(read, bookID)
	log.Printf("Updated book %d for %s, set read status to %v", bookID, person, read)
	return err
}

// --- END NEW ---

// Modified crawl_dua to not require the ticker argument
func crawl_dua(db *sql.DB) {
	// The main page is a better starting point to find all books
	currenturl := "https://www.service95.com/book-club/"

	if err := os.MkdirAll(imagesDir, os.ModePerm); err != nil {
		log.Println("Error creating images directory:", err)
	}

	c := colly.NewCollector(
		colly.AllowedDomains("www.service95.com"),
		colly.MaxDepth(1), // Only visit links on the main page
	)

	bookCollector := c.Clone()

	// Find links to individual book pages
	c.OnHTML("a", func(e *colly.HTMLElement) {
		link := e.Attr("href")
		// Make sure it's a valid book link and not something else
		if strings.Contains(link, "/books/") {
			bookCollector.Visit(e.Request.AbsoluteURL(link))
		}
	})

	c.OnRequest(func(r *colly.Request) {
		fmt.Println("Visiting", r.URL)
	})

	// On the book page, extract the details
	bookCollector.OnHTML(`div`, func(e *colly.HTMLElement) {
		title := e.ChildText("h1")
		imgSrc := e.ChildAttr("img", "src")
		var month string
		e.ForEachWithBreak("p", func(_ int, el *colly.HTMLElement) bool {
			if strings.Contains(el.Text, "Monthly") {
				month = el.Text
				return false // stop iterating
			}
			return true // continue iterating
		})

		if title == "" || month == "" || !strings.Contains(imgSrc, "uploads") {
			return
		}

		exists, err := bookExists(db, title)
		if err != nil {
			log.Printf("Error checking if book '%s' exists: %v", title, err)
			return
		}
		if exists {
			log.Println("SKipping book: ", title)
			return // Silently skip if already exists
		}

		absoluteImgURL := e.Request.AbsoluteURL(imgSrc)
		localImagePath, err := downloadImage(absoluteImgURL)
		if err != nil {
			log.Printf("Failed to download image for '%s': %v", title, err)
			return
		}

		newBook := Book{Name: title, ImagePath: localImagePath}
		err = addBook(db, newBook)
		if err != nil {
			log.Printf("Failed to add book '%s' to database: %v", title, err)
		} else {
			log.Printf("Successfully added book: %s", title)
		}
	})

	c.Visit(currenturl)
	c.Wait()
	bookCollector.Wait()
}

func downloadImage(url string) (string, error) {
	response, err := http.Get(url)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()

	if response.StatusCode != http.StatusOK {
		return "", fmt.Errorf("received non-200 response code: %d", response.StatusCode)
	}

	// Use a UUID to prevent filename collisions
	id := uuid.New().String() + filepath.Ext(url)
	localPath := filepath.Join(imagesDir, id)
	file, err := os.Create(localPath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	_, err = io.Copy(file, response.Body)
	if err != nil {
		return "", err
	}

	return "/" + filepath.ToSlash(localPath), nil
}

func addBook(db *sql.DB, book Book) error {
	// The new columns have default values, so we don't need to specify them here.
	insertSQL := `INSERT INTO books (name, image_path) VALUES (?, ?)`
	statement, err := db.Prepare(insertSQL)
	if err != nil {
		return err
	}
	defer statement.Close()
	_, err = statement.Exec(book.Name, book.ImagePath)
	return err
}

func bookExists(db *sql.DB, name string) (bool, error) {
	var count int
	query := `SELECT COUNT(*) FROM books WHERE name = ?`
	err := db.QueryRow(query, name).Scan(&count)
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

// getAllBooks now retrieves the read status as well.
func getAllBooks(db *sql.DB) ([]Book, error) {
	// --- UPDATED: Select the new columns ---
	query := "SELECT id, name, image_path, tuomas_read, jenni_read FROM books ORDER BY id DESC"
	rows, err := db.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var books []Book
	for rows.Next() {
		var b Book
		// --- UPDATED: Scan the new columns into the struct ---
		if err := rows.Scan(&b.ID, &b.Name, &b.ImagePath, &b.TuomasRead, &b.JenniRead); err != nil {
			return nil, err
		}
		books = append(books, b)
	}
	return books, nil
}
