package instance

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/jmoiron/sqlx"
)

type Store struct {
	db *sqlx.DB
}

func NewStore(db *sqlx.DB) *Store {
	return &Store{db: db}
}

func OccurrenceKey(fingerprint string, startsAt time.Time) string {
	sum := sha256.Sum256([]byte(fingerprint + "\x00" + startsAt.UTC().Format(time.RFC3339Nano)))
	return hex.EncodeToString(sum[:])
}

// GetOrCreate creates a neutral occurrence. ApplyObservation changes lifecycle
// state only after the matching event observation has been inserted.
func (s *Store) GetOrCreate(ctx context.Context, snap *Snapshot) (*Instance, bool, error) {
	key := OccurrenceKey(snap.Fingerprint, snap.StartsAt)
	existing, err := s.GetByOccurrenceKey(ctx, key)
	if err != nil {
		return nil, false, err
	}
	if existing != nil {
		return existing, false, nil
	}

	labels, err := json.Marshal(snap.Labels)
	if err != nil {
		return nil, false, fmt.Errorf("marshal instance labels: %w", err)
	}
	annotations, err := json.Marshal(snap.Annotations)
	if err != nil {
		return nil, false, fmt.Errorf("marshal instance annotations: %w", err)
	}

	const q = `
		INSERT INTO alert_instance (
			occurrence_key, fingerprint, alertname, classification, state,
			labels, annotations, generator_url, starts_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)
	`
	res, err := s.db.ExecContext(ctx, q,
		key, snap.Fingerprint, snap.Alertname, snap.Classification, StateUnknown,
		labels, annotations, snap.GeneratorURL, snap.StartsAt,
	)
	if err != nil {
		return nil, false, fmt.Errorf("insert alert instance: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return nil, false, fmt.Errorf("get alert instance id: %w", err)
	}
	got, err := s.GetByID(ctx, id)
	return got, got != nil && got.ObservationCount == 0, err
}

func (s *Store) GetByID(ctx context.Context, id int64) (*Instance, error) {
	const q = `SELECT * FROM alert_instance WHERE id = ?`
	var inst Instance
	if err := s.db.GetContext(ctx, &inst, q, id); err != nil {
		return nil, fmt.Errorf("get alert instance: %w", err)
	}
	return &inst, nil
}

func (s *Store) GetByOccurrenceKey(ctx context.Context, key string) (*Instance, error) {
	const q = `SELECT * FROM alert_instance WHERE occurrence_key = ?`
	var inst Instance
	if err := s.db.GetContext(ctx, &inst, q, key); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("find alert instance: %w", err)
	}
	return &inst, nil
}

// ApplyObservation projects one immutable event onto its occurrence exactly
// once. The guard, instance update, and event classification share a
// transaction, so a webhook retry repairs a partially handled event without
// increasing observation_count twice.
func (s *Store) ApplyObservation(
	ctx context.Context,
	eventID, id int64,
	status string,
	endsAt *time.Time,
	observedAt time.Time,
	snap *Snapshot,
) (observationType string, applied bool, err error) {
	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return "", false, fmt.Errorf("begin observation transaction: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	const guard = `
		INSERT IGNORE INTO alert_instance_observation_applied (event_id, instance_id)
		VALUES (?, ?)
	`
	res, err := tx.ExecContext(ctx, guard, eventID, id)
	if err != nil {
		return "", false, fmt.Errorf("claim instance observation: %w", err)
	}
	claimed, err := res.RowsAffected()
	if err != nil {
		return "", false, err
	}
	if claimed == 0 {
		if err := tx.GetContext(ctx, &observationType, `SELECT observation_type FROM event WHERE id = ?`, eventID); err != nil {
			return "", false, fmt.Errorf("get applied observation type: %w", err)
		}
		if err := tx.Commit(); err != nil {
			return "", false, err
		}
		return observationType, false, nil
	}

	var inst Instance
	if err := tx.GetContext(ctx, &inst, `SELECT * FROM alert_instance WHERE id = ? FOR UPDATE`, id); err != nil {
		return "", false, fmt.Errorf("lock alert instance: %w", err)
	}

	observationType = classifyObservation(inst.State, inst.ObservationCount, status)
	historyIncomplete := inst.HistoryIncomplete
	firstObservedAt := inst.FirstObservedAt
	if status == StateResolved && inst.ObservationCount == 0 {
		historyIncomplete = true
	}
	if firstObservedAt == nil {
		firstObservedAt = &observedAt
	}

	labels, err := json.Marshal(snap.Labels)
	if err != nil {
		return "", false, err
	}
	annotations, err := json.Marshal(snap.Annotations)
	if err != nil {
		return "", false, err
	}

	var resolvedObservedAt *time.Time
	if status == StateResolved {
		resolvedObservedAt = &observedAt
	}
	const q = `
		UPDATE alert_instance
		SET state = ?, labels = ?, annotations = ?, generator_url = ?,
			ends_at = ?, first_observed_at = ?, last_observed_at = ?,
			resolved_observed_at = ?,
			observation_count = observation_count + 1,
			history_incomplete = ?
		WHERE id = ?
	`
	if _, err = tx.ExecContext(ctx, q,
		status, labels, annotations, snap.GeneratorURL,
		endsAt, firstObservedAt, observedAt, resolvedObservedAt,
		historyIncomplete, id,
	); err != nil {
		return "", false, fmt.Errorf("apply instance observation: %w", err)
	}
	if _, err := tx.ExecContext(ctx, `UPDATE event SET observation_type = ? WHERE id = ?`, observationType, eventID); err != nil {
		return "", false, fmt.Errorf("classify event observation: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return "", false, fmt.Errorf("commit instance observation: %w", err)
	}
	return observationType, true, nil
}

func classifyObservation(previousState string, observationCount int, status string) string {
	if status == StateFiring {
		if observationCount == 0 || previousState == StateUnknown || previousState == StateResolved {
			return ObservationFirstFiring
		}
		return ObservationRepeatFiring
	}
	return ObservationResolved
}

func (s *Store) BindAlert(ctx context.Context, alertID string, instanceID int64) (bool, error) {
	const q = `
		INSERT IGNORE INTO alert_instance_binding (alert_id, instance_id)
		VALUES (?, ?)
	`
	res, err := s.db.ExecContext(ctx, q, alertID, instanceID)
	if err != nil {
		return false, fmt.Errorf("bind instance to alert: %w", err)
	}
	n, err := res.RowsAffected()
	return n == 1, err
}

func (s *Store) FindOpenBoundAlertID(ctx context.Context, instanceID int64) (string, error) {
	const q = `
		SELECT b.alert_id
		FROM alert_instance_binding b
		JOIN alert a ON a.id = b.alert_id
		WHERE b.instance_id = ? AND a.state != 'resolved'
		ORDER BY b.bound_at DESC
		LIMIT 1
	`
	var alertID string
	if err := s.db.GetContext(ctx, &alertID, q, instanceID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return "", nil
		}
		return "", fmt.Errorf("find open bound alert: %w", err)
	}
	return alertID, nil
}

func (s *Store) ListByAlertID(ctx context.Context, alertID string) ([]Instance, error) {
	const q = `
		SELECT i.*
		FROM alert_instance i
		JOIN alert_instance_binding b ON b.instance_id = i.id
		WHERE b.alert_id = ?
		ORDER BY i.starts_at, i.id
	`
	var instances []Instance
	if err := s.db.SelectContext(ctx, &instances, q, alertID); err != nil {
		return nil, fmt.Errorf("list alert instances: %w", err)
	}
	return instances, nil
}

func (s *Store) CountFiringByAlertID(ctx context.Context, alertID string) (int, error) {
	const q = `
		SELECT COUNT(*)
		FROM alert_instance i
		JOIN alert_instance_binding b ON b.instance_id = i.id
		WHERE b.alert_id = ? AND i.state = 'firing'
	`
	var count int
	if err := s.db.GetContext(ctx, &count, q, alertID); err != nil {
		return 0, fmt.Errorf("count firing instances: %w", err)
	}
	return count, nil
}
