// 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"
	"strings"
	"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>
`

const adminLoginHTML = `
<!doctype html>
<html>
<head>
    <title>YASUC Admin - Login</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="password"] { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        button { background: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #005a87; }
        .error { color: red; margin-top: 10px; }
    </style>
</head>
<body>
    <h2>YASUC Admin Login</h2>
    <form method="POST" action="/admin/login">
        <div class="form-group">
            <label for="password">Senha:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Entrar</button>
        {{if .Error}}
        <div class="error">{{.Error}}</div>
        {{end}}
    </form>
</body>
</html>
`

const adminDashboardHTML = `
<!doctype html>
<html>
<head>
    <title>YASUC Admin - Dashboard</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
        .logout { background: #dc3545; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px; }
        .logout:hover { background: #c82333; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background-color: #f8f9fa; }
        .content-preview { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .delete-btn { background: #dc3545; color: white; padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; }
        .delete-btn:hover { background: #c82333; }
        .view-btn { background: #007cba; color: white; padding: 5px 10px; text-decoration: none; border-radius: 3px; margin-right: 5px; }
        .view-btn:hover { background: #005a87; }
        .stats { background: #f8f9fa; padding: 15px; border-radius: 4px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>YASUC Admin Dashboard</h1>
        <a href="/admin/logout" class="logout">Sair</a>
    </div>
    
    <div class="stats">
        <strong>Estatísticas:</strong> {{.TotalPastes}} pastes total | {{.TotalSize}} bytes
    </div>
    
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>URL</th>
                <th>Conteúdo</th>
                <th>Tamanho</th>
                <th>Criado em</th>
                <th>Ações</th>
            </tr>
        </thead>
        <tbody>
            {{range .Pastes}}
            <tr>
                <td>{{.ShortID}}</td>
                <td><a href="/{{.ShortID}}" target="_blank">{{.ShortID}}</a></td>
                <td class="content-preview">{{.ContentPreview}}</td>
                <td>{{.Size}} bytes</td>
                <td>{{.CreatedAt}}</td>
                <td>
                    <a href="/{{.ShortID}}" class="view-btn" target="_blank">Ver</a>
                    <button class="delete-btn" onclick="deletePaste('{{.ShortID}}')">Remover</button>
                </td>
            </tr>
            {{end}}
        </tbody>
    </table>
    
    <script>
        function deletePaste(shortID) {
            if (confirm('Tem certeza que deseja remover o paste ' + shortID + '?')) {
                fetch('/admin/delete/' + shortID, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    }
                }).then(response => {
                    if (response.ok) {
                        location.reload();
                    } else {
                        alert('Erro ao remover paste');
                    }
                });
            }
        }
    </script>
</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 Paste struct {
	ShortID        string
	ContentPreview string
	Size           int
	CreatedAt      string
}

func getAllPastes(db *sql.DB) ([]Paste, error) {
	rows, err := db.Query(`
		SELECT short_id, content, created_at 
		FROM pastes 
		ORDER BY created_at DESC
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var pastes []Paste
	for rows.Next() {
		var p Paste
		var content string
		var createdAt int64
		err := rows.Scan(&p.ShortID, &content, &createdAt)
		if err != nil {
			return nil, err
		}

		p.Size = len(content)
		if len(content) > 50 {
			p.ContentPreview = content[:50] + "..."
		} else {
			p.ContentPreview = content
		}

		p.CreatedAt = time.Unix(createdAt, 0).Format("02/01/2006 15:04:05")
		pastes = append(pastes, p)
	}

	return pastes, nil
}

func deletePaste(db *sql.DB, shortID string) error {
	result, err := db.Exec("DELETE FROM pastes WHERE short_id = ?", shortID)
	if err != nil {
		return err
	}

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

	if rowsAffected == 0 {
		return pasteNotFound{}
	}

	return nil
}

func getStats(db *sql.DB) (totalPastes int, totalSize int64, err error) {
	err = db.QueryRow("SELECT COUNT(*), SUM(LENGTH(content)) FROM pastes").Scan(&totalPastes, &totalSize)
	if err == sql.ErrNoRows {
		return 0, 0, nil
	}
	return totalPastes, totalSize, err
}

type handler struct {
	db            *sql.DB
	adminPassword string
}

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 (h *handler) adminLogin(w http.ResponseWriter, req *http.Request) {
	if req.Method == http.MethodPost {
		password := req.FormValue("password")
		if password == h.adminPassword {
			// Set session cookie
			http.SetCookie(w, &http.Cookie{
				Name:     "admin_session",
				Value:    "authenticated",
				Path:     "/admin",
				MaxAge:   3600, // 1 hour
				HttpOnly: true,
			})
			http.Redirect(w, req, "/admin/dashboard", http.StatusSeeOther)
			return
		}

		// Show error
		tmpl, _ := template.New("login").Parse(adminLoginHTML)
		data := struct{ Error string }{"Senha incorreta"}
		tmpl.Execute(w, data)
		return
	}

	// Show login form
	tmpl, _ := template.New("login").Parse(adminLoginHTML)
	data := struct{ Error string }{""}
	tmpl.Execute(w, data)
}

func (h *handler) adminLogout(w http.ResponseWriter, req *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:     "admin_session",
		Value:    "",
		Path:     "/admin",
		MaxAge:   -1,
		HttpOnly: true,
	})
	http.Redirect(w, req, "/admin/login", http.StatusSeeOther)
}

func (h *handler) checkAuth(w http.ResponseWriter, req *http.Request) bool {
	cookie, err := req.Cookie("admin_session")
	if err != nil || cookie.Value != "authenticated" {
		http.Redirect(w, req, "/admin/login", http.StatusSeeOther)
		return false
	}
	return true
}

func (h *handler) adminDashboard(w http.ResponseWriter, req *http.Request) {
	if !h.checkAuth(w, req) {
		return
	}

	pastes, err := getAllPastes(h.db)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	totalPastes, totalSize, err := getStats(h.db)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	data := struct {
		Pastes      []Paste
		TotalPastes int
		TotalSize   int64
	}{
		Pastes:      pastes,
		TotalPastes: totalPastes,
		TotalSize:   totalSize,
	}

	tmpl, _ := template.New("dashboard").Parse(adminDashboardHTML)
	tmpl.Execute(w, data)
}

func (h *handler) adminDelete(w http.ResponseWriter, req *http.Request) {
	if !h.checkAuth(w, req) {
		return
	}

	if req.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Extract shortID from URL path
	pathParts := strings.Split(req.URL.Path, "/")
	if len(pathParts) != 4 {
		http.Error(w, "Invalid URL", http.StatusBadRequest)
		return
	}

	shortID := pathParts[3]
	err := deletePaste(h.db, shortID)
	if err != nil {
		if _, ok := err.(pasteNotFound); ok {
			http.Error(w, "Paste not found", http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Paste deleted successfully"))
}

func newHandler(db *sql.DB, adminPassword string) http.Handler {
	h := handler{db: db, adminPassword: adminPassword}
	mux := http.NewServeMux()
	mux.HandleFunc("/", h.alles)
	mux.HandleFunc("/admin/login", h.adminLogin)
	mux.HandleFunc("/admin/logout", h.adminLogout)
	mux.HandleFunc("/admin/dashboard", h.adminDashboard)
	mux.HandleFunc("/admin/delete/", h.adminDelete)
	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)
	}

	// Get admin password from environment variable
	adminPassword := os.Getenv("YASUC_ADMIN_PASSWORD")
	if adminPassword == "" {
		adminPassword = "admin123" // Default password
		log.Println("Warning: YASUC_ADMIN_PASSWORD not set, using default password 'admin123'")
	}

	// 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, adminPassword))
	sockAddr := fmt.Sprintf("%s:%d", addr, port)
	log.Fatal(http.ListenAndServe(sockAddr, nil))
}
