package telegram

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"git3.techno-world.net/lrosales/broad-announce/internal/postgres"
)

// Handler executes parsed Commands against the database. It
// is the bridge between incoming bot messages and the
// individuals / subscriptions tables.
//
// Handler is safe to call from multiple goroutines (it
// acquires a fresh connection per call from the pool).
type Handler struct {
	Pool   *postgres.Pool
	Logger *slog.Logger
}

// NewHandler returns a Handler.
func NewHandler(pool *postgres.Pool, logger *slog.Logger) *Handler {
	return &Handler{Pool: pool, Logger: logger}
}

// ErrNotLinked is returned when a command requires the
// individual to be linked (telegram_user_id set) but isn't.
// The handler turns this into a "please /start first" reply.
var ErrNotLinked = errors.New("telegram account not linked; run /start <invite_code> first")

// Handle dispatches one parsed command. The returned string
// is the text to reply to the user (may be empty for
// non-applicable commands).
//
// Handle is idempotent w.r.t. /start (re-running with the
// same code is a no-op) and /subscribe (re-running upserts
// the subscription).
func (h *Handler) Handle(ctx context.Context, msg *Message) (string, error) {
	if msg == nil || msg.From == nil {
		return "", nil
	}
	cmd := Parse(msg.Text)
	h.Logger.Info("telegram command",
		"user_id", msg.From.ID,
		"chat_id", msg.Chat.ID,
		"raw", cmd.Raw,
	)

	switch {
	case cmd.Start != nil:
		return h.handleStart(ctx, msg, cmd.Start)
	case cmd.Subscribe != nil:
		return h.handleSubscribe(ctx, msg, cmd.Subscribe)
	case cmd.Unsubscribe != nil:
		return h.handleUnsubscribe(ctx, msg, cmd.Unsubscribe)
	case cmd.Preferences:
		return h.handlePreferences(ctx, msg)
	case cmd.Status != nil:
		return h.handleStatus(ctx, msg, cmd.Status)
	case cmd.Mute != nil:
		return h.handleMute(ctx, msg, cmd.Mute)
	case cmd.Unmute:
		return h.handleUnmute(ctx, msg)
	default:
		// Unknown / non-command. Don't reply.
		return "", nil
	}
}

// handleStart links the telegram_user_id to the individual
// whose telegram_invite_code matches.
//
// SPEC §8: "admin must create the individual and issue the
// code first; unknown users are rejected." The handler
// returns a friendly error if the invite code is unknown.
func (h *Handler) handleStart(ctx context.Context, msg *Message, c *StartCmd) (string, error) {
	if c.InviteCode == "" {
		return "Welcome. Please run /start <your_invite_code> (your admin should have given you the code).", nil
	}
	// Atomic: claim the invite code for this user, only if it
	// hasn't been claimed by someone else.
	tag, err := h.Pool.Exec(ctx, `
		UPDATE individuals
		   SET telegram_user_id = $1,
		       telegram_chat_id = $2::text,
		       telegram_invite_code = NULL
		 WHERE telegram_invite_code = $3
		   AND (telegram_user_id IS NULL OR telegram_user_id = $1)
		RETURNING id, full_name
	`, msg.From.ID, fmt.Sprintf("%d", msg.Chat.ID), c.InviteCode)
	if err != nil {
		return "", fmt.Errorf("start update: %w", err)
	}
	if tag.RowsAffected() == 0 {
		// Either unknown code or already linked to a
		// different user. Disambiguate.
		var existing string
		err := h.Pool.QueryRow(ctx, `
			SELECT COALESCE(telegram_user_id::text, 'NULL')
			  FROM individuals WHERE telegram_invite_code = $1
		`, c.InviteCode).Scan(&existing)
		if err == nil {
			return fmt.Sprintf("Invite code %q is already linked to another account.", c.InviteCode), nil
		}
		return fmt.Sprintf("Unknown invite code %q. Please check with your admin.", c.InviteCode), nil
	}
	// Fetch the individual to greet them by name.
	var name string
	_ = h.Pool.QueryRow(ctx, `
		SELECT full_name FROM individuals
		 WHERE telegram_user_id = $1
	`, msg.From.ID).Scan(&name)
	if name == "" {
		name = "operator"
	}
	return fmt.Sprintf("Linked. Welcome, %s.\n\nRun /preferences to see your current subscriptions, or /subscribe <source_id> [min_severity] to opt in.", name), nil
}

// handleSubscribe requires the individual to be linked
// (we look them up by telegram_user_id). It upserts a
// subscription with the given source_id and min_severity.
func (h *Handler) handleSubscribe(ctx context.Context, msg *Message, c *SubscribeCmd) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	// Validate the source exists in this company.
	var ok bool
	err = h.Pool.QueryRow(ctx, `
		SELECT EXISTS (
		    SELECT 1 FROM sources WHERE company_id = $1 AND id = $2
		)
	`, ind.CompanyID, c.SourceID).Scan(&ok)
	if err != nil {
		return "", fmt.Errorf("source check: %w", err)
	}
	if !ok {
		return fmt.Sprintf("Unknown source %q for your company. Run /preferences to see what's available.", c.SourceID), nil
	}
	_, err = h.Pool.Exec(ctx, `
		INSERT INTO subscriptions
		    (individual_id, company_id, source_id, min_severity, channel_mask, status)
		VALUES ($1, $2, $3, $4, '["fcm","telegram"]'::jsonb, 'active')
		ON CONFLICT (individual_id, source_id) DO UPDATE
		    SET min_severity = EXCLUDED.min_severity,
		        status       = 'active',
		        channel_mask = '["fcm","telegram"]'::jsonb
	`, ind.ID, ind.CompanyID, c.SourceID, c.MinSeverity)
	if err != nil {
		return "", fmt.Errorf("subscribe upsert: %w", err)
	}
	return fmt.Sprintf("Subscribed to %s (min severity: %s).", c.SourceID, c.MinSeverity), nil
}

// handleUnsubscribe soft-deletes a subscription by setting
// status='paused'. We keep the row so /subscribe re-enables
// the same row without a new id.
func (h *Handler) handleUnsubscribe(ctx context.Context, msg *Message, c *UnsubscribeCmd) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	tag, err := h.Pool.Exec(ctx, `
		UPDATE subscriptions
		   SET status = 'paused'
		 WHERE individual_id = $1 AND source_id = $2
	`, ind.ID, c.SourceID)
	if err != nil {
		return "", fmt.Errorf("unsubscribe: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Sprintf("You were not subscribed to %s.", c.SourceID), nil
	}
	return fmt.Sprintf("Unsubscribed from %s.", c.SourceID), nil
}

// handlePreferences shows the current subscriptions and
// the global mute_until timestamp.
func (h *Handler) handlePreferences(ctx context.Context, msg *Message) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	rows, err := h.Pool.Query(ctx, `
		SELECT source_id, min_severity, channel_mask, status
		  FROM subscriptions
		 WHERE individual_id = $1
		 ORDER BY source_id
	`, ind.ID)
	if err != nil {
		return "", fmt.Errorf("preferences query: %w", err)
	}
	defer rows.Close()
	var lines []string
	for rows.Next() {
		var src, minSev, status string
		var channels []byte
		if err := rows.Scan(&src, &minSev, &channels, &status); err != nil {
			return "", err
		}
		if minSev == "" {
			minSev = "any"
		}
		lines = append(lines, fmt.Sprintf("  • %s [min=%s, channels=%s, %s]",
			src, minSev, string(channels), status))
	}
	if len(lines) == 0 {
		lines = append(lines, "  (no subscriptions yet)")
	}
	reply := "Your subscriptions:\n" + joinLines(lines)
	if ind.MuteUntil != nil && ind.MuteUntil.After(time.Now()) {
		reply += fmt.Sprintf("\n\nMuted until %s UTC.", ind.MuteUntil.UTC().Format("2006-01-02 15:04"))
	} else {
		reply += "\n\nNot muted."
	}
	return reply, nil
}

// handleStatus lists the last N deliveries for this
// individual. M3 ships a simple per-individual view.
func (h *Handler) handleStatus(ctx context.Context, msg *Message, c *StatusCmd) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	rows, err := h.Pool.Query(ctx, `
		SELECT alert_id, channel, status, sent_at
		  FROM deliveries
		 WHERE individual_id = $1
		 ORDER BY id DESC
		 LIMIT $2
	`, ind.ID, c.Limit)
	if err != nil {
		return "", fmt.Errorf("status query: %w", err)
	}
	defer rows.Close()
	var lines []string
	for rows.Next() {
		var alertID, channel, status string
		var sentAt *time.Time
		if err := rows.Scan(&alertID, &channel, &status, &sentAt); err != nil {
			return "", err
		}
		ts := "(pending)"
		if sentAt != nil {
			ts = sentAt.UTC().Format("15:04:05")
		}
		short := alertID
		if len(short) > 12 {
			short = short[:12]
		}
		lines = append(lines, fmt.Sprintf("  • %s %s [%s] %s", ts, short, channel, status))
	}
	if len(lines) == 0 {
		lines = append(lines, "  (no deliveries yet)")
	}
	return "Recent deliveries:\n" + joinLines(lines), nil
}

// handleMute sets mute_until to now+duration (or now+until).
func (h *Handler) handleMute(ctx context.Context, msg *Message, c *MuteCmd) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	var until time.Time
	if c.Duration > 0 {
		until = time.Now().UTC().Add(c.Duration)
	} else {
		until = c.Until
	}
	_, err = h.Pool.Exec(ctx, `
		UPDATE individuals SET mute_until = $1 WHERE id = $2
	`, until, ind.ID)
	if err != nil {
		return "", fmt.Errorf("mute: %w", err)
	}
	return fmt.Sprintf("Muted until %s UTC.", until.Format("2006-01-02 15:04")), nil
}

// handleUnmute clears mute_until.
func (h *Handler) handleUnmute(ctx context.Context, msg *Message) (string, error) {
	ind, err := h.lookupByTelegramUser(ctx, msg.From.ID)
	if err != nil {
		return err.Error(), nil
	}
	_, err = h.Pool.Exec(ctx, `
		UPDATE individuals SET mute_until = NULL WHERE id = $1
	`, ind.ID)
	if err != nil {
		return "", fmt.Errorf("unmute: %w", err)
	}
	return "Unmuted.", nil
}

// individual is the small projection of individuals we need
// for command handling.
type individual struct {
	ID         string
	CompanyID  string
	FullName   string
	MuteUntil  *time.Time
}

// lookupByTelegramUser returns the individual linked to a
// given telegram_user_id, or ErrNotLinked.
func (h *Handler) lookupByTelegramUser(ctx context.Context, telegramUserID int64) (*individual, error) {
	var ind individual
	err := h.Pool.QueryRow(ctx, `
		SELECT id, company_id, full_name, mute_until
		  FROM individuals
		 WHERE telegram_user_id = $1
	`, telegramUserID).Scan(&ind.ID, &ind.CompanyID, &ind.FullName, &ind.MuteUntil)
	if err != nil {
		return nil, ErrNotLinked
	}
	return &ind, nil
}

func joinLines(ls []string) string {
	out := ""
	for i, s := range ls {
		if i > 0 {
			out += "\n"
		}
		out += s
	}
	return out
}
