package backend

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os/exec"
	"strings"

	"github.com/emersion/go-ical"
	"github.com/emersion/go-webdav"
	"github.com/emersion/go-webdav/caldav"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/crypto/bcrypt"
	"nxcaldav/internal/config"
)

type DBBackend struct {
	pool          *pgxpool.Pool
	redactionText string
}

func NewDBBackend(ctx context.Context, cfg *config.Config) (*DBBackend, error) {
	pool, err := pgxpool.New(ctx, cfg.Database.URL)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to postgres: %v", err)
	}

	b := &DBBackend{
		pool:          pool,
		redactionText: cfg.Server.Redaction,
	}

	if err := b.initSchema(ctx); err != nil {
		return nil, err
	}

	if err := b.syncConfig(ctx, cfg); err != nil {
		return nil, err
	}

	return b, nil
}

func (b *DBBackend) initSchema(ctx context.Context) error {
	queries := []string{
		`CREATE TABLE IF NOT EXISTS users (
			id SERIAL PRIMARY KEY,
			name TEXT UNIQUE NOT NULL,
			password TEXT NOT NULL
		)`,
		`CREATE TABLE IF NOT EXISTS calendars (
			id SERIAL PRIMARY KEY,
			owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
			path TEXT UNIQUE NOT NULL,
			name TEXT,
			description TEXT
		)`,
		`CREATE TABLE IF NOT EXISTS calendar_access (
			calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,
			user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
			mode TEXT NOT NULL,
			PRIMARY KEY (calendar_id, user_id)
		)`,
		`CREATE TABLE IF NOT EXISTS calendar_objects (
			id SERIAL PRIMARY KEY,
			calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,
			path TEXT NOT NULL,
			data TEXT NOT NULL,
			etag TEXT NOT NULL,
			UNIQUE (calendar_id, path)
		)`,
	}

	for _, q := range queries {
		if _, err := b.pool.Exec(ctx, q); err != nil {
			return fmt.Errorf("failed to execute schema query: %v", err)
		}
	}
	return nil
}

func (b *DBBackend) resolvePassword(u config.User) (string, error) {
	var raw string
	if u.PasswordCmd != "" {
		cmd := exec.Command("bash", "-c", u.PasswordCmd)
		out, err := cmd.Output()
		if err != nil {
			return "", fmt.Errorf("failed to run password command for %s: %v", u.Name, err)
		}
		raw = strings.TrimSpace(string(out))
	} else {
		raw = u.Password
	}

	// If it already looks like a bcrypt hash, return as is.
	// bcrypt hashes usually start with $2a$ or $2b$ or $2y$.
	if strings.HasPrefix(raw, "$2") {
		return raw, nil
	}

	// Otherwise, hash it.
	hash, err := bcrypt.GenerateFromPassword([]byte(raw), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(hash), nil
}

func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error {
	tx, err := b.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)

	configUserNames := make(map[string]bool)
	configCalendarPaths := make(map[string]bool)

	// Sync Users
	for _, u := range cfg.Users {
		configUserNames[u.Name] = true
		hashed, err := b.resolvePassword(u)
		if err != nil {
			return err
		}

		_, err = tx.Exec(ctx, `
			INSERT INTO users (name, password) VALUES ($1, $2)
			ON CONFLICT (name) DO UPDATE SET password = EXCLUDED.password`,
			u.Name, hashed)
		if err != nil {
			return err
		}
	}

	// Sync Calendars and Access
	for _, c := range cfg.Calendars {
		path := fmt.Sprintf("/%s/calendars/%s/", c.Owner, c.ID)
		configCalendarPaths[path] = true

		var ownerID int
		err := tx.QueryRow(ctx, "SELECT id FROM users WHERE name = $1", c.Owner).Scan(&ownerID)
		if err != nil {
			return fmt.Errorf("owner %s not found: %v", c.Owner, err)
		}

		var calID int
		err = tx.QueryRow(ctx, `
			INSERT INTO calendars (owner_id, path, name) VALUES ($1, $2, $3)
			ON CONFLICT (path) DO UPDATE SET owner_id = EXCLUDED.owner_id, name = EXCLUDED.name
			RETURNING id`,
			ownerID, path, c.ID).Scan(&calID)
		if err != nil {
			return err
		}

		_, err = tx.Exec(ctx, "DELETE FROM calendar_access WHERE calendar_id = $1", calID)
		if err != nil {
			return err
		}

		for _, a := range c.Access {
			var userID int
			err := tx.QueryRow(ctx, "SELECT id FROM users WHERE name = $1", a.User).Scan(&userID)
			if err != nil {
				return fmt.Errorf("access user %s not found: %v", a.User, err)
			}
			_, err = tx.Exec(ctx, "INSERT INTO calendar_access (calendar_id, user_id, mode) VALUES ($1, $2, $3)",
				calID, userID, a.Mode)
			if err != nil {
				return err
			}
		}
	}

	// Orphaned User Check
	userRows, err := tx.Query(ctx, "SELECT name FROM users")
	if err != nil {
		return err
	}
	var dbUsers []string
	for userRows.Next() {
		var name string
		if err := userRows.Scan(&name); err != nil {
			return err
		}
		dbUsers = append(dbUsers, name)
	}
	userRows.Close()

	for _, name := range dbUsers {
		if !configUserNames[name] {
			log.Printf("WARNING: Orphaned user found in database: %s (Not in config.yaml)", name)
		}
	}

	// Orphaned Calendar Check
	calRows, err := tx.Query(ctx, "SELECT path FROM calendars")
	if err != nil {
		return err
	}
	var dbPaths []string
	for calRows.Next() {
		var p string
		if err := calRows.Scan(&p); err != nil {
			return err
		}
		dbPaths = append(dbPaths, p)
	}
	calRows.Close()

	for _, p := range dbPaths {
		if !configCalendarPaths[p] {
			log.Printf("WARNING: Orphaned calendar found in database: %s (Not in config.yaml)", p)
		}
	}
	
	return tx.Commit(ctx)
}

func (b *DBBackend) VerifyUser(ctx context.Context, username, password string) (bool, error) {
	var hash string
	err := b.pool.QueryRow(ctx, "SELECT password FROM users WHERE name = $1", username).Scan(&hash)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return false, nil
		}
		return false, err
	}

	err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil, nil
}

// UserPrincipalBackend implementation

func (b *DBBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
	principal, ok := ctx.Value("principal").(string)
	if !ok {
		return "", errors.New("no principal in context")
	}
	return principal, nil
}

func (b *DBBackend) getUsername(ctx context.Context) (string, error) {
	principal, err := b.CurrentUserPrincipal(ctx)
	if err != nil {
		return "", err
	}
	return strings.Trim(principal, "/"), nil
}

func (b *DBBackend) checkAccess(ctx context.Context, calendarPath string, requiredMode string) (int, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return 0, err
	}

	if !strings.HasSuffix(calendarPath, "/") {
		calendarPath += "/"
	}

	var calID int
	var ownerName string
	err = b.pool.QueryRow(ctx, `
		SELECT c.id, u.name 
		FROM calendars c 
		JOIN users u ON c.owner_id = u.id 
		WHERE c.path = $1`, calendarPath).Scan(&calID, &ownerName)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return 0, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar not found"))
		}
		return 0, err
	}

	if ownerName == username {
		return calID, nil
	}

	var mode string
	err = b.pool.QueryRow(ctx, `
		SELECT mode FROM calendar_access 
		WHERE calendar_id = $1 AND user_id = (SELECT id FROM users WHERE name = $2)`,
		calID, username).Scan(&mode)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return 0, webdav.NewHTTPError(http.StatusForbidden, errors.New("access denied"))
		}
		return 0, err
	}

	if requiredMode == "write" && mode != "read-write" {
		return 0, webdav.NewHTTPError(http.StatusForbidden, errors.New("read-only access"))
	}

	return calID, nil
}

func (b *DBBackend) filterCalendar(ctx context.Context, calendarID int, original *ical.Calendar) (*ical.Calendar, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return nil, err
	}

	var ownerName string
	err = b.pool.QueryRow(ctx, "SELECT u.name FROM users u JOIN calendars c ON c.owner_id = u.id WHERE c.id = $1", calendarID).Scan(&ownerName)
	if err != nil {
		return nil, err
	}

	if username == ownerName {
		return original, nil
	}

	filtered := ical.NewCalendar()
	filtered.Props = original.Props

	for _, child := range original.Children {
		if child.Name != "VEVENT" && child.Name != "VTODO" {
			filtered.Children = append(filtered.Children, child)
			continue
		}

		class := "PUBLIC"
		if prop := child.Props.Get("CLASS"); prop != nil {
			class = strings.ToUpper(prop.Value)
		}

		switch class {
		case "PRIVATE":
			continue
		case "CONFIDENTIAL":
			redacted := ical.NewComponent(child.Name)
			propsToKeep := []string{
				"UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "CLASS",
				"DUE", "COMPLETED", "STATUS", "PRIORITY", "PERCENT-COMPLETE",
			}
			for _, pName := range propsToKeep {
				if props, ok := child.Props[pName]; ok {
					redacted.Props[pName] = props
				}
			}
			redacted.Props.SetText("SUMMARY", b.redactionText)
			filtered.Children = append(filtered.Children, redacted)
		default:
			filtered.Children = append(filtered.Children, child)
		}
	}

	if len(filtered.Children) == 0 {
		return nil, nil
	}

	return filtered, nil
}

// CalDAV Backend implementation

func (b *DBBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("/%s/calendars/", username), nil
}

func (b *DBBackend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return nil, err
	}

	rows, err := b.pool.Query(ctx, `
		SELECT path, name FROM calendars 
		WHERE owner_id = (SELECT id FROM users WHERE name = $1)
		OR id IN (SELECT calendar_id FROM calendar_access WHERE user_id = (SELECT id FROM users WHERE name = $1))`,
		username)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var res []caldav.Calendar
	for rows.Next() {
		var cal caldav.Calendar
		if err := rows.Scan(&cal.Path, &cal.Name); err != nil {
			return nil, err
		}
		cal.SupportedComponentSet = []string{"VEVENT", "VTODO"}
		res = append(res, cal)
	}
	return res, nil
}

func (b *DBBackend) GetCalendar(ctx context.Context, path string) (*caldav.Calendar, error) {
	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	var cal caldav.Calendar
	err := b.pool.QueryRow(ctx, "SELECT path, name FROM calendars WHERE path = $1", path).Scan(&cal.Path, &cal.Name)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar not found"))
		}
		return nil, err
	}
	cal.SupportedComponentSet = []string{"VEVENT", "VTODO"}
	return &cal, nil
}

func (b *DBBackend) CreateCalendar(ctx context.Context, calendar *caldav.Calendar) error {
	return webdav.NewHTTPError(http.StatusForbidden, errors.New("calendar creation only via config"))
}

func (b *DBBackend) ListCalendarObjects(ctx context.Context, path string, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
	calID, err := b.checkAccess(ctx, path, "read")
	if err != nil {
		return nil, err
	}

	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	rows, err := b.pool.Query(ctx, "SELECT path, data, etag FROM calendar_objects WHERE calendar_id = $1", calID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var res []caldav.CalendarObject
	for rows.Next() {
		var obj caldav.CalendarObject
		var dataStr string
		if err := rows.Scan(&obj.Path, &dataStr, &obj.ETag); err != nil {
			return nil, err
		}

		calData, err := ical.NewDecoder(strings.NewReader(dataStr)).Decode()
		if err != nil {
			return nil, err
		}

		filteredData, err := b.filterCalendar(ctx, calID, calData)
		if err != nil {
			return nil, err
		}
		if filteredData == nil {
			continue
		}
		obj.Data = filteredData
		res = append(res, obj)
	}
	return res, nil
}

func (b *DBBackend) GetCalendarObject(ctx context.Context, path string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	calID, err := b.checkAccess(ctx, parentPath, "read")
	if err != nil {
		return nil, err
	}

	var obj caldav.CalendarObject
	var dataStr string
	err = b.pool.QueryRow(ctx, "SELECT path, data, etag FROM calendar_objects WHERE calendar_id = $1 AND path = $2", calID, path).Scan(&obj.Path, &dataStr, &obj.ETag)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
		}
		return nil, err
	}

	calData, err := ical.NewDecoder(strings.NewReader(dataStr)).Decode()
	if err != nil {
		return nil, err
	}

	filteredData, err := b.filterCalendar(ctx, calID, calData)
	if err != nil {
		return nil, err
	}
	if filteredData == nil {
		return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
	}
	obj.Data = filteredData
	return &obj, nil
}

func (b *DBBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	calID, err := b.checkAccess(ctx, parentPath, "write")
	if err != nil {
		return nil, err
	}

	var buf bytes.Buffer
	if err := ical.NewEncoder(&buf).Encode(calendar); err != nil {
		return nil, err
	}
	dataStr := buf.String()
	etag := fmt.Sprintf(`"%d"`, len(calendar.Events()))

	_, err = b.pool.Exec(ctx, `
		INSERT INTO calendar_objects (calendar_id, path, data, etag) VALUES ($1, $2, $3, $4)
		ON CONFLICT (calendar_id, path) DO UPDATE SET data = EXCLUDED.data, etag = EXCLUDED.etag`,
		calID, path, dataStr, etag)
	if err != nil {
		return nil, err
	}

	return &caldav.CalendarObject{
		Path: path,
		Data: calendar,
		ETag: etag,
	}, nil
}

func (b *DBBackend) DeleteCalendarObject(ctx context.Context, path string) error {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	calID, err := b.checkAccess(ctx, parentPath, "write")
	if err != nil {
		return err
	}

	commandTag, err := b.pool.Exec(ctx, "DELETE FROM calendar_objects WHERE calendar_id = $1 AND path = $2", calID, path)
	if err != nil {
		return err
	}
	if commandTag.RowsAffected() == 0 {
		return webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
	}
	return nil
}

func (b *DBBackend) QueryCalendarObjects(ctx context.Context, path string, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
	return b.ListCalendarObjects(ctx, path, nil)
}
