package main

import (
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"fn-registry/functions/infra"
)

// Issue 0128: adjuntos de archivos por card.

const (
	maxUploadBytes = 10 << 20 // 10 MiB
	uploadsSubdir  = "uploads"
)

type CardFile struct {
	ID         string `json:"id"`
	CardID     string `json:"card_id"`
	UploaderID string `json:"uploader_id"`
	Filename   string `json:"filename"`
	MIME       string `json:"mime"`
	Size       int64  `json:"size"`
	Source     string `json:"source"`
	URL        string `json:"url"`
	CreatedAt  string `json:"created_at"`
}

func (db *DB) CreateCardFile(cardID, uploaderID, filename, mimeType, storedPath, source string, size int64) (*CardFile, error) {
	id := newID()
	now := nowRFC3339()
	if source == "" {
		source = "upload"
	}
	_, err := db.conn.Exec(
		`INSERT INTO card_files
            (id, card_id, uploader_id, filename, mime, size, stored_path, source, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		id, cardID, uploaderID, filename, mimeType, size, storedPath, source, now,
	)
	if err != nil {
		return nil, err
	}
	return &CardFile{
		ID:         id,
		CardID:     cardID,
		UploaderID: uploaderID,
		Filename:   filename,
		MIME:       mimeType,
		Size:       size,
		Source:     source,
		URL:        "/api/files/" + id,
		CreatedAt:  now,
	}, nil
}

func (db *DB) ListCardFiles(cardID string) ([]CardFile, error) {
	rows, err := db.conn.Query(
		`SELECT id, card_id, uploader_id, filename, mime, size, source, created_at
           FROM card_files
          WHERE card_id = ? AND deleted_at IS NULL
          ORDER BY created_at ASC`,
		cardID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []CardFile{}
	for rows.Next() {
		var f CardFile
		if err := rows.Scan(&f.ID, &f.CardID, &f.UploaderID, &f.Filename, &f.MIME, &f.Size, &f.Source, &f.CreatedAt); err != nil {
			return nil, err
		}
		f.URL = "/api/files/" + f.ID
		out = append(out, f)
	}
	return out, rows.Err()
}

type storedCardFile struct {
	ID         string
	CardID     string
	Filename   string
	MIME       string
	Size       int64
	StoredPath string
}

func (db *DB) GetCardFile(id string) (*storedCardFile, error) {
	var f storedCardFile
	err := db.conn.QueryRow(
		`SELECT id, card_id, filename, mime, size, stored_path
           FROM card_files
          WHERE id = ? AND deleted_at IS NULL`,
		id,
	).Scan(&f.ID, &f.CardID, &f.Filename, &f.MIME, &f.Size, &f.StoredPath)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &f, nil
}

func (db *DB) SoftDeleteCardFile(id string) (int64, error) {
	res, err := db.conn.Exec(
		`UPDATE card_files SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL`,
		nowRFC3339(), id,
	)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}

func uploadsDir(workdir string) string {
	return filepath.Join(workdir, uploadsSubdir)
}

func safeFilename(name string) string {
	name = filepath.Base(name)
	name = strings.ReplaceAll(name, string(os.PathSeparator), "_")
	name = strings.ReplaceAll(name, "/", "_")
	name = strings.ReplaceAll(name, "\\", "_")
	name = strings.TrimSpace(name)
	if name == "" || name == "." || name == ".." {
		return "file"
	}
	return name
}

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

// POST /api/cards/{id}/files (multipart, field "file", optional "source")
func handleUploadCardFile(db *DB, workdir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		cardID := r.PathValue("id")
		if cardID == "" {
			badRequest(w, "card id required")
			return
		}

		r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes+1<<20)
		if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
			badRequest(w, "multipart parse: "+err.Error())
			return
		}

		file, header, err := r.FormFile("file")
		if err != nil {
			badRequest(w, "missing 'file' field: "+err.Error())
			return
		}
		defer file.Close()

		if header.Size > maxUploadBytes {
			infra.HTTPErrorResponse(w, infra.HTTPError{
				Status: http.StatusRequestEntityTooLarge,
				Code:   "file_too_large",
				Message: fmt.Sprintf("file exceeds %d bytes", maxUploadBytes),
			})
			return
		}

		source := r.FormValue("source")
		switch source {
		case "", "upload":
			source = "upload"
		case "description", "chat":
			// keep
		default:
			source = "upload"
		}

		dir := filepath.Join(uploadsDir(workdir), cardID)
		if err := os.MkdirAll(dir, 0o755); err != nil {
			serverError(w, fmt.Errorf("mkdir uploads: %w", err))
			return
		}

		fname := safeFilename(header.Filename)
		storedPath := filepath.Join(dir, randomFilePrefix()+"__"+fname)

		out, err := os.Create(storedPath)
		if err != nil {
			serverError(w, fmt.Errorf("create file: %w", err))
			return
		}
		written, copyErr := io.Copy(out, file)
		closeErr := out.Close()
		if copyErr != nil {
			os.Remove(storedPath)
			serverError(w, fmt.Errorf("write file: %w", copyErr))
			return
		}
		if closeErr != nil {
			os.Remove(storedPath)
			serverError(w, fmt.Errorf("close file: %w", closeErr))
			return
		}
		if written > maxUploadBytes {
			os.Remove(storedPath)
			infra.HTTPErrorResponse(w, infra.HTTPError{
				Status:  http.StatusRequestEntityTooLarge,
				Code:    "file_too_large",
				Message: fmt.Sprintf("file exceeds %d bytes", maxUploadBytes),
			})
			return
		}

		mimeType := header.Header.Get("Content-Type")
		if mimeType == "" {
			mimeType = mime.TypeByExtension(filepath.Ext(fname))
		}
		if mimeType == "" {
			mimeType = "application/octet-stream"
		}

		actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)

		cf, err := db.CreateCardFile(cardID, actor, fname, mimeType, storedPath, source, written)
		if err != nil {
			os.Remove(storedPath)
			serverError(w, err)
			return
		}

		infra.HTTPJSONResponse(w, http.StatusCreated, cf)
	}
}

// GET /api/cards/{id}/files
func handleListCardFiles(db *DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		cardID := r.PathValue("id")
		if cardID == "" {
			badRequest(w, "card id required")
			return
		}
		files, err := db.ListCardFiles(cardID)
		if err != nil {
			serverError(w, err)
			return
		}
		infra.HTTPJSONResponse(w, http.StatusOK, files)
	}
}

// GET /api/files/{id}
func handleServeFile(db *DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		if id == "" {
			badRequest(w, "file id required")
			return
		}
		f, err := db.GetCardFile(id)
		if err != nil {
			serverError(w, err)
			return
		}
		if f == nil {
			notFound(w, "file not found")
			return
		}
		fh, err := os.Open(f.StoredPath)
		if err != nil {
			notFound(w, "file missing on disk")
			return
		}
		defer fh.Close()
		if f.MIME != "" {
			w.Header().Set("Content-Type", f.MIME)
		}
		w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size))
		disposition := "inline"
		if !isInlineMIME(f.MIME) {
			disposition = "attachment"
		}
		w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, sanitizeHeaderFilename(f.Filename)))
		w.Header().Set("Cache-Control", "private, max-age=3600")
		_, _ = io.Copy(w, fh)
	}
}

// DELETE /api/files/{id}
func handleDeleteCardFile(db *DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		if id == "" {
			badRequest(w, "file id required")
			return
		}
		n, err := db.SoftDeleteCardFile(id)
		if err != nil {
			serverError(w, err)
			return
		}
		if n == 0 {
			notFound(w, "file not found")
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func isInlineMIME(m string) bool {
	if m == "" {
		return false
	}
	m = strings.ToLower(m)
	switch {
	case strings.HasPrefix(m, "image/"):
		return true
	case m == "application/pdf":
		return true
	case strings.HasPrefix(m, "text/"):
		return true
	}
	return false
}

func sanitizeHeaderFilename(name string) string {
	name = strings.ReplaceAll(name, `"`, "")
	name = strings.ReplaceAll(name, "\n", "")
	name = strings.ReplaceAll(name, "\r", "")
	return name
}
