package vfs

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"log/slog"
	"reflect"
	"time"

	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/trace"
)

var (
	meter  = otel.Meter("git.kmsign.ru/royalcat/tstor/src/vfs")
	tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/vfs")
)

type fsTelemetry struct {
	openedFiles metric.Int64UpDownCounter
}

type LogFS struct {
	fs  Filesystem
	log *rlog.Logger
	tel *fsTelemetry

	timeout     time.Duration
	readTimeout time.Duration
}

func isLoggableError(err error) bool {
	return err != nil && !errors.Is(err, fs.ErrNotExist)
}

var _ Filesystem = (*LogFS)(nil)

func WrapLogFS(vfs Filesystem) (*LogFS, error) {
	openedFiles, err := meter.Int64UpDownCounter("vfs.opened_files")
	if err != nil {
		return nil, fmt.Errorf("failed to create opened_files metric: %w", err)
	}

	return &LogFS{
		fs:          vfs,
		log:         rlog.Component("logfs"),
		tel:         &fsTelemetry{openedFiles: openedFiles},
		timeout:     time.Minute * 3,
		readTimeout: time.Minute,
	}, nil
}

// ModTime implements Filesystem.
func (lfs *LogFS) ModTime() time.Time {
	return lfs.ModTime()
}

// Mode implements Filesystem.
func (lfs *LogFS) Mode() fs.FileMode {
	return lfs.Mode()
}

// Size implements Filesystem.
func (lfs *LogFS) Size() int64 {
	return lfs.Size()
}

// Sys implements Filesystem.
func (lfs *LogFS) Sys() any {
	return lfs.Sys()
}

func (fs *LogFS) FsName() string {
	return "logfs"
}

func (fs *LogFS) traceAttrs(add ...attribute.KeyValue) trace.SpanStartOption {
	return trace.WithAttributes(append([]attribute.KeyValue{
		attribute.String("fs", fs.FsName()),
	}, add...)...)
}

// Info implements Filesystem.
func (fs *LogFS) Info() (fs.FileInfo, error) {
	return fs.fs.Info()
}

// IsDir implements Filesystem.
func (fs *LogFS) IsDir() bool {
	return fs.fs.IsDir()
}

// Name implements Filesystem.
func (fs *LogFS) Name() string {
	return fs.fs.Name()
}

// Type implements Filesystem.
func (fs *LogFS) Type() fs.FileMode {
	return fs.fs.Type()
}

// Open implements Filesystem.
func (fs *LogFS) Open(ctx context.Context, filename string) (file File, err error) {
	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "Open",
		fs.traceAttrs(attribute.String("filename", filename)),
	)
	defer func() {
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	file, err = fs.fs.Open(ctx, filename)
	if isLoggableError(err) {
		fs.log.Error(ctx, "Failed to open file")
	}
	file = wrapLogFile(file, filename, fs.log, fs.readTimeout, fs.tel)

	if file != nil {
		fs.tel.openedFiles.Add(ctx, 1)
	}

	return file, err
}

// ReadDir implements Filesystem.
func (fs *LogFS) ReadDir(ctx context.Context, path string) (entries []fs.DirEntry, err error) {
	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "ReadDir",
		fs.traceAttrs(
			attribute.String("path", path),
			attribute.String("fs-type", reflect.TypeOf(fs.fs).Name()),
		),
	)
	defer func() {
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	entries, err = fs.fs.ReadDir(ctx, path)
	if isLoggableError(err) {
		fs.log.Error(ctx, "Failed to read dir", rlog.Error(err))
	}
	return entries, err
}

// Stat implements Filesystem.
func (lfs *LogFS) Stat(ctx context.Context, filename string) (info fs.FileInfo, err error) {
	ctx, cancel := context.WithTimeout(ctx, lfs.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "Stat",
		lfs.traceAttrs(attribute.String("filename", filename)),
	)
	defer func() {
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	info, err = lfs.fs.Stat(ctx, filename)
	if isLoggableError(err) {
		lfs.log.Error(ctx, "Failed to stat", rlog.Error(err))
	}
	return info, err
}

// Unlink implements Filesystem.
func (fs *LogFS) Unlink(ctx context.Context, filename string) (err error) {
	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "Unlink",
		fs.traceAttrs(attribute.String("filename", filename)),
	)
	defer func() {
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	err = fs.fs.Unlink(ctx, filename)
	if isLoggableError(err) {
		fs.log.Error(ctx, "Failed to stat", rlog.Error(err))
	}
	return err
}

type LogFile struct {
	filename string
	f        File

	log *rlog.Logger
	tel *fsTelemetry

	timeout time.Duration
}

// Name implements File.
func (f *LogFile) Name() string {
	return f.f.Name()
}

// Type implements File.
func (f *LogFile) Type() fs.FileMode {
	return f.f.Type()
}

var _ File = (*LogFile)(nil)

func wrapLogFile(f File, filename string, log *rlog.Logger, timeout time.Duration, tel *fsTelemetry) *LogFile {
	return &LogFile{
		filename: filename,
		f:        f,
		log:      log.With(slog.String("filename", filename)),
		tel:      tel,
		timeout:  timeout,
	}
}

// Close implements File.
func (f *LogFile) Close(ctx context.Context) (err error) {
	ctx, cancel := context.WithTimeout(ctx, f.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "Close",
		trace.WithAttributes(attribute.String("filename", f.filename)),
	)
	defer func() {
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	err = f.f.Close(ctx)
	if isLoggableError(err) {
		f.log.Error(ctx, "Failed to close", rlog.Error(err))
	}

	if err != nil {
		f.tel.openedFiles.Add(ctx, -1)
	}

	return err
}

// IsDir implements File.
func (f *LogFile) IsDir() bool {
	return f.f.IsDir()
}

// Read implements File.
func (f *LogFile) Read(ctx context.Context, p []byte) (n int, err error) {
	ctx, cancel := context.WithTimeout(ctx, f.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "Read",
		trace.WithAttributes(
			attribute.String("filename", f.filename),
			attribute.Int("length", len(p)),
		),
	)
	defer func() {
		span.SetAttributes(attribute.Int("read", n))
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	n, err = f.f.Read(ctx, p)
	if isLoggableError(err) {
		f.log.Error(ctx, "Failed to read", rlog.Error(err))
	}
	return n, err
}

// ReadAt implements File.
func (f *LogFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
	ctx, cancel := context.WithTimeout(ctx, f.timeout)
	defer cancel()
	ctx, span := tracer.Start(ctx, "ReadAt",
		trace.WithAttributes(
			attribute.String("filename", f.filename),
			attribute.Int("length", len(p)),
			attribute.Int64("offset", off),
		),
	)
	defer func() {
		span.SetAttributes(attribute.Int("read", n))
		if err != nil {
			span.RecordError(err)
		}
		span.End()
	}()

	n, err = f.f.ReadAt(ctx, p, off)
	if isLoggableError(err) {
		f.log.Error(ctx, "Failed to read")
	}
	return n, err
}

// Size implements File.
func (f *LogFile) Size() int64 {
	return f.f.Size()
}

// Stat implements File.
func (f *LogFile) Info() (fs.FileInfo, error) {
	info, err := f.f.Info()
	if isLoggableError(err) {
		f.log.Error(context.Background(), "Failed to info", rlog.Error(err))
	}
	return info, err
}