package adapters

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"git.max-richter.dev/max/marka/parser"
)

type LocalFsAdapter struct {
	roots []string
	cache map[string]*Entry
	mu    sync.RWMutex
}

func (l *LocalFsAdapter) readDir(path string, root string) (*Entry, error) {
	dirInfo, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	entries, err := os.ReadDir(path)
	if err != nil {
		return nil, err
	}

	out := make([]*Entry, 0, len(entries))
	for _, e := range entries {
		info, _ := e.Info()

		entryType := "dir"
		if !e.IsDir() {
			entryType = "file"
		}

		var content any
		var mime string
		var size int64
		if !e.IsDir() {
			mime = contentTypeFor(e.Name())
			if info != nil {
				size = info.Size()
			}
			if mime == "application/markdown" {
				entryPath := filepath.Join(path, e.Name())
				fileContent, err := os.ReadFile(entryPath)
				if err == nil {
					parsedContent, err := parser.ParseFile(string(fileContent))
					if err == nil {
						content = parsedContent
					}
				}
			}
		}

		childPath, err := filepath.Rel(root, filepath.Join(path, e.Name()))
		if err != nil {
			return nil, err
		}
		responseChildPath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), childPath))

		out = append(out, &Entry{
			Name:    e.Name(),
			Path:    responseChildPath,
			Type:    entryType,
			ModTime: info.ModTime(),
			MIME:    mime,
			Size:    size,
			Content: content,
		})
	}

	relPath, err := filepath.Rel(root, path)
	if err != nil {
		return nil, err
	}
	if relPath == "." {
		relPath = ""
	}

	responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))

	return &Entry{
		Type:    "dir",
		Name:    filepath.Base(responsePath),
		Path:    responsePath,
		ModTime: dirInfo.ModTime(),
		Content: out,
	}, nil
}

func (l *LocalFsAdapter) readFile(path string, root string) (*Entry, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	relPath, err := filepath.Rel(root, path)
	if err != nil {
		return nil, err
	}

	responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
	mime := contentTypeFor(path)
	var content any

	fileContent, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	if mime == "application/markdown" {
		parsedContent, err := parser.ParseFile(string(fileContent))
		if err == nil {
			content = parsedContent
		} else {
			fmt.Println(err)
			// Fallback to raw content on parsing error
			content = fileContent
		}
	} else {
		content = fileContent
	}

	return &Entry{
		Type:    "file",
		Name:    fi.Name(),
		Path:    responsePath,
		ModTime: fi.ModTime(),
		MIME:    mime,
		Size:    fi.Size(),
		Content: content,
	}, nil
}

func (l *LocalFsAdapter) Read(path string) (*Entry, error) {
	if path == "/" {
		var latestModTime time.Time
		for _, r := range l.roots {
			info, err := os.Stat(r)
			if err != nil {
				continue
			}
			if info.ModTime().After(latestModTime) {
				latestModTime = info.ModTime()
			}
		}

		l.mu.RLock()
		cached, found := l.cache[path]
		l.mu.RUnlock()

		if found && !latestModTime.After(cached.ModTime) {
			return cached, nil
		}

		entries := make([]*Entry, 0, len(l.roots))
		for _, r := range l.roots {
			info, err := os.Stat(r)
			if err != nil {
				continue
			}
			name := filepath.Base(r)
			entries = append(entries, &Entry{
				Name:    name,
				Path:    "/" + name,
				Type:    "dir",
				ModTime: info.ModTime(),
			})
		}

		entry := &Entry{
			Type:    "dir",
			Name:    "/",
			Path:    "/",
			ModTime: latestModTime,
			Content: entries,
		}

		l.mu.Lock()
		l.cache[path] = entry
		l.mu.Unlock()
		return entry, nil
	}

	pathParts := strings.Split(strings.Trim(path, "/"), "/")
	if len(pathParts) == 0 || pathParts[0] == "" {
		return nil, ErrNotFound
	}

	rootIdentifier := pathParts[0]
	var targetRoot string
	for _, r := range l.roots {
		if filepath.Base(r) == rootIdentifier {
			targetRoot = r
			break
		}
	}

	if targetRoot == "" {
		return nil, ErrNotFound
	}

	subPath := filepath.Join(pathParts[1:]...)
	target := filepath.Join(targetRoot, subPath)

	absTarget, err := filepath.Abs(target)
	if err != nil {
		return nil, err
	}
	absRoot, err := filepath.Abs(targetRoot)
	if err != nil {
		return nil, err
	}
	if !strings.HasPrefix(absTarget, absRoot) {
		return nil, errors.New("path escapes root")
	}

	fi, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, ErrNotFound
		}

		return nil, err
	}

	l.mu.RLock()
	cached, found := l.cache[path]
	l.mu.RUnlock()

	if found && !fi.ModTime().After(cached.ModTime) {
		return cached, nil
	}

	var newEntry *Entry
	if fi.IsDir() {
		newEntry, err = l.readDir(target, targetRoot)
	} else {
		newEntry, err = l.readFile(target, targetRoot)
	}

	if err != nil {
		return nil, err
	}

	l.mu.Lock()
	l.cache[path] = newEntry
	l.mu.Unlock()

	return newEntry, nil
}

func (l *LocalFsAdapter) Write(path string, content []byte) error {
	pathParts := strings.Split(strings.Trim(path, "/"), "/")
	if len(pathParts) == 0 || pathParts[0] == "" {
		return errors.New("invalid path")
	}

	rootIdentifier := pathParts[0]
	var targetRoot string
	for _, r := range l.roots {
		if filepath.Base(r) == rootIdentifier {
			targetRoot = r
			break
		}
	}

	if targetRoot == "" {
		return ErrNotFound
	}

	subPath := filepath.Join(pathParts[1:]...)
	target := filepath.Join(targetRoot, subPath)

	absTarget, err := filepath.Abs(target)
	if err != nil {
		return err
	}
	absRoot, err := filepath.Abs(targetRoot)
	if err != nil {
		return err
	}
	if !strings.HasPrefix(absTarget, absRoot) {
		return errors.New("path escapes root")
	}

	dir := filepath.Dir(absTarget)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	err = os.WriteFile(absTarget, content, 0o644)
	if err != nil {
		return err
	}

	// Invalidate cache
	l.mu.Lock()
	defer l.mu.Unlock()
	currentPath := path
	for {
		delete(l.cache, currentPath)
		if currentPath == "/" {
			break
		}
		lastSlash := strings.LastIndex(currentPath, "/")
		if lastSlash <= 0 {
			currentPath = "/"
		} else {
			currentPath = currentPath[:lastSlash]
		}
	}

	return nil
}

func NewLocalFsAdapter(roots []string) (FileAdapter, error) {
	return &LocalFsAdapter{
		roots: roots,
		cache: make(map[string]*Entry),
	}, nil
}
