// yasuc - yet another sprunge.us clone (command line pastebin)
//
// Copyright (C) Tom Jakubowski <tom@crystae.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"flag"
	"fmt"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"os"
	"time"

	_ "modernc.org/sqlite"
)

const (
	maxPasteSize  = 4 * 1024 * 1024
	shortIDLength = 7
)

const usageText = `
<!doctype html>
<html>
  <head>
  </head>
  <body>
    <pre>
yasuc(1)                          YASUC                          yasuc(1)

NAME
    yasuc - command line pastebin.

SYNOPSIS
    &lt;command&gt; | curl -F 'sprunge=&lt;-' {{.BaseURL}}

DESCRIPTION
    A command line pastebin.  Pastes are immutable and created with simple HTTP
    POST requests. The path of a paste's URL is a short unique identifier.

EXAMPLE
    $ echo 'hello world' | curl -F 'sprunge=&lt;-' {{.BaseURL}}
       {{.BaseURL}}/Ly34kx3
    $ firefox {{.BaseURL}}/Ly34kx3

COPYRIGHT
    Copyright © Tom Jakubowski.  License AGPLv3: GNU Affero GPL Version 3
    <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">https://www.gnu.org/licenses/agpl-3.0.en.html</a>.

    This is free software; you are free to change and redistribute it.  There
    is NO WARRANTY, to the extent permitted by law.  For copying conditions and
    source, see <a href="https://github.com/tomjakubowski/yasuc">https://github.com/tomjakubowski/yasuc</a>.

SEE ALSO
    <a href="https://github.com/tomjakubowski/yasuc">http://github.com/tomjakubowski/yasuc</a>
    </pre>
  </body>
</html>
`

type pasteTooLarge struct{}

func (e pasteTooLarge) Error() string {
	return fmt.Sprintf("paste too large (maximum size %d bytes)", maxPasteSize)
}

// generateShortID gera um ID único de 7 caracteres
func generateShortID() string {
	const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	b := make([]byte, shortIDLength)
	for i := range b {
		b[i] = charset[rand.Intn(len(charset))]
	}
	return string(b)
}

// generateUniqueShortID gera um ID único que não existe no banco
func generateUniqueShortID(db *sql.DB) (string, error) {
	maxAttempts := 100
	for i := 0; i < maxAttempts; i++ {
		id := generateShortID()

		// Verificar se o ID já existe
		var exists int
		err := db.QueryRow("SELECT 1 FROM pastes WHERE short_id = ? LIMIT 1", id).Scan(&exists)
		if err == sql.ErrNoRows {
			return id, nil
		}
		if err != nil {
			return "", err
		}
	}
	return "", fmt.Errorf("não foi possível gerar um ID único após %d tentativas", maxAttempts)
}

func stashPaste(db *sql.DB, pasteStr string) (key string, err error) {
	if len(pasteStr) > maxPasteSize {
		err = pasteTooLarge{}
		return
	}

	// Gerar ID curto único
	shortID, err := generateUniqueShortID(db)
	if err != nil {
		return "", err
	}

	// Calcular hash SHA-256 para verificação de duplicatas
	paste := []byte(pasteStr)
	hash := sha256.Sum256(paste)
	hashStr := hex.EncodeToString(hash[:])

	// Verificar se já existe um paste com o mesmo conteúdo
	var existingID string
	err = db.QueryRow("SELECT short_id FROM pastes WHERE content_hash = ? LIMIT 1", hashStr).Scan(&existingID)
	if err == nil {
		// Paste já existe, retornar o ID existente
		return existingID, nil
	} else if err != sql.ErrNoRows {
		return "", err
	}

	// Inserir novo paste
	_, err = db.Exec("INSERT INTO pastes (short_id, content, content_hash, created_at) VALUES (?, ?, ?, ?)",
		shortID, pasteStr, hashStr, time.Now().Unix())
	if err != nil {
		return "", err
	}

	return shortID, nil
}

type pasteNotFound struct{}

func (e pasteNotFound) Error() string {
	return "not found"
}

func fetchPaste(db *sql.DB, key string) (paste string, err error) {
	var content string
	err = db.QueryRow("SELECT content FROM pastes WHERE short_id = ?", key).Scan(&content)
	if err != nil {
		if err == sql.ErrNoRows {
			return "", pasteNotFound{}
		}
		return "", err
	}
	return content, nil
}

type handler struct {
	db *sql.DB
}

func (h *handler) alles(w http.ResponseWriter, req *http.Request) {
	if req.URL.Path != "/" {
		paste, err := fetchPaste(h.db, req.URL.Path[1:])
		if err != nil {
			if _, ok := err.(pasteNotFound); ok {
				http.Error(w, "not found", http.StatusNotFound)
			} else {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}
		fmt.Fprintf(w, "%s", paste)
		return
	}

	if req.Method == http.MethodPost {
		body := req.FormValue("sprunge")
		// sprunge.us proceeds if there's no form data in 'sprunge', and just makes
		// an empty paste.
		key, err := stashPaste(h.db, body)
		if err != nil {
			if _, ok := err.(pasteTooLarge); ok {
				http.Error(w, err.Error(), http.StatusNotAcceptable)
			} else {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}
		baseURL := fmt.Sprintf("https://%s", req.Host)
		fmt.Fprintf(w, "%s/%s\n", baseURL, key)
		return
	}
	// usage message
	tmpl, err := template.New("usage").Parse(usageText)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	baseURL := fmt.Sprintf("https://%s", req.Host)
	data := struct{ BaseURL string }{baseURL}
	_ = tmpl.Execute(w, data)
}

func newHandler(db *sql.DB) http.Handler {
	h := handler{db}
	mux := http.NewServeMux()
	mux.HandleFunc("/", h.alles)
	return mux
}

func initDatabase(db *sql.DB) error {
	// Create the pastes table if it doesn't exist
	_, err := db.Exec(`
		CREATE TABLE IF NOT EXISTS pastes (
			short_id TEXT PRIMARY KEY,
			content TEXT NOT NULL,
			content_hash TEXT NOT NULL,
			created_at INTEGER NOT NULL
		)
	`)
	if err != nil {
		return err
	}

	// Create indexes for better performance
	_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_content_hash ON pastes(content_hash)`)
	if err != nil {
		return err
	}

	_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at)`)
	return err
}

func main() {
	// Initialize random seed
	rand.Seed(time.Now().UnixNano())

	var dbPath, addr string
	var port int
	flag.StringVar(&dbPath, "db", "", "location of database file")
	flag.StringVar(&addr, "addr", "", "bind address")
	flag.IntVar(&port, "port", 9001, "bind port")
	flag.Parse()
	if dbPath == "" {
		fmt.Fprintf(os.Stderr, "db option is required\n")
		os.Exit(1)
	}

	// Open SQLite database
	db, err := sql.Open("sqlite", dbPath)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = db.Close() }()

	// Initialize database schema
	err = initDatabase(db)
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/", newHandler(db))
	sockAddr := fmt.Sprintf("%s:%d", addr, port)
	log.Fatal(http.ListenAndServe(sockAddr, nil))
}
