package windows

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/hex"
	"runtime"
	"sync"
	"unsafe"

	"github.com/rs/zerolog/log"
	"golang.org/x/sys/windows"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

const (
	MEM_PRIVATE = 0x20000
)

func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	if proc.Status == model.StatusOffline {
		return "", errors.ErrWeChatOffline
	}

	// Open process handle
	handle, err := windows.OpenProcess(windows.PROCESS_VM_READ|windows.PROCESS_QUERY_INFORMATION, false, proc.PID)
	if err != nil {
		return "", errors.OpenProcessFailed(err)
	}
	defer windows.CloseHandle(handle)

	// Create context to control all goroutines
	searchCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create channels for memory data and results
	memoryChannel := make(chan []byte, 100)
	resultChannel := make(chan string, 1)

	// Determine number of worker goroutines
	workerCount := runtime.NumCPU()
	if workerCount < 2 {
		workerCount = 2
	}
	if workerCount > MaxWorkers {
		workerCount = MaxWorkers
	}
	log.Debug().Msgf("Starting %d workers for V4 key search", workerCount)

	// Start consumer goroutines
	var workerWaitGroup sync.WaitGroup
	workerWaitGroup.Add(workerCount)
	for index := 0; index < workerCount; index++ {
		go func() {
			defer workerWaitGroup.Done()
			e.worker(searchCtx, handle, memoryChannel, resultChannel)
		}()
	}

	// Start producer goroutine
	var producerWaitGroup sync.WaitGroup
	producerWaitGroup.Add(1)
	go func() {
		defer producerWaitGroup.Done()
		defer close(memoryChannel) // Close channel when producer is done
		err := e.findMemory(searchCtx, handle, memoryChannel)
		if err != nil {
			log.Err(err).Msg("Failed to find memory regions")
		}
	}()

	// Wait for producer and consumers to complete
	go func() {
		producerWaitGroup.Wait()
		workerWaitGroup.Wait()
		close(resultChannel)
	}()

	// Wait for result
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case result, ok := <-resultChannel:
		if ok && result != "" {
			return result, nil
		}
	}

	return "", errors.ErrNoValidKey
}

// findMemoryV4 searches for writable memory regions for V4 version
func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, memoryChannel chan<- []byte) error {
	// Define search range
	minAddr := uintptr(0x10000)    // Process space usually starts from 0x10000
	maxAddr := uintptr(0x7FFFFFFF) // 32-bit process space limit

	if runtime.GOARCH == "amd64" {
		maxAddr = uintptr(0x7FFFFFFFFFFF) // 64-bit process space limit
	}
	log.Debug().Msgf("Scanning memory regions from 0x%X to 0x%X", minAddr, maxAddr)

	currentAddr := minAddr

	for currentAddr < maxAddr {
		var memInfo windows.MemoryBasicInformation
		err := windows.VirtualQueryEx(handle, currentAddr, &memInfo, unsafe.Sizeof(memInfo))
		if err != nil {
			break
		}

		// Skip small memory regions
		if memInfo.RegionSize < 1024*1024 {
			currentAddr += uintptr(memInfo.RegionSize)
			continue
		}

		// Check if memory region is readable and private
		if memInfo.State == windows.MEM_COMMIT && (memInfo.Protect&windows.PAGE_READWRITE) != 0 && memInfo.Type == MEM_PRIVATE {
			// Calculate region size, ensure it doesn't exceed limit
			regionSize := uintptr(memInfo.RegionSize)
			if currentAddr+regionSize > maxAddr {
				regionSize = maxAddr - currentAddr
			}

			// Read memory region
			memory := make([]byte, regionSize)
			if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
				select {
				case memoryChannel <- memory:
					log.Debug().Msgf("Memory region for analysis: 0x%X - 0x%X, size: %d bytes", currentAddr, currentAddr+regionSize, regionSize)
				case <-ctx.Done():
					return nil
				}
			}
		}

		// Move to next memory region
		currentAddr = uintptr(memInfo.BaseAddress) + uintptr(memInfo.RegionSize)
	}

	return nil
}

// workerV4 processes memory regions to find V4 version key

// validateKey validates a single key candidate
func (e *V4Extractor) validateKey(handle windows.Handle, addr uint64) string {
	keyData := make([]byte, 0x20) // 32-byte key
	if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
		return ""
	}

	// Validate key against database header
	if e.validator.Validate(keyData) {
		return hex.EncodeToString(keyData)
	}

	return ""
}

func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {
	keyPattern := []byte{
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	}
	ptrSize := 8
	littleEndianFunc := binary.LittleEndian.Uint64
	for {
		select {
		case <-ctx.Done():
			return
		case memory, ok := <-memoryChannel:
			if !ok {
				return
			}
			index := len(memory)
			for {
				select {
				case <-ctx.Done():
					return
				default:
				}
				index = bytes.LastIndex(memory[:index], keyPattern)
				if index == -1 || index-ptrSize < 0 {
					break
				}
				ptrValue := littleEndianFunc(memory[index-ptrSize : index])
				if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
					if key := e.validateKey(handle, ptrValue); key != "" {
						select {
						case resultChannel <- key:
							log.Debug().Msg("Valid key found: " + key)
							return
						default:
						}
					}
				}
				index -= 1
			}
		}
	}
}

package windows

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"runtime"
	"sync"
	"unsafe"

	"github.com/rs/zerolog/log"
	"golang.org/x/sys/windows"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/model"
	"github.com/sjzar/chatlog/pkg/util"
)

const (
	V3ModuleName = "WeChatWin.dll"
	MaxWorkers   = 16
)

func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	if proc.Status == model.StatusOffline {
		return "", errors.ErrWeChatOffline
	}

	// Open WeChat process
	handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, proc.PID)
	if err != nil {
		return "", errors.OpenProcessFailed(err)
	}
	defer windows.CloseHandle(handle)

	// Check process architecture
	is64Bit, err := util.Is64Bit(handle)
	if err != nil {
		return "", err
	}

	// Create context to control all goroutines
	searchCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create channels for memory data and results
	memoryChannel := make(chan []byte, 100)
	resultChannel := make(chan string, 1)

	// Determine number of worker goroutines
	workerCount := runtime.NumCPU()
	if workerCount < 2 {
		workerCount = 2
	}
	if workerCount > MaxWorkers {
		workerCount = MaxWorkers
	}
	log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)

	// Start consumer goroutines
	var workerWaitGroup sync.WaitGroup
	workerWaitGroup.Add(workerCount)
	for index := 0; index < workerCount; index++ {
		go func() {
			defer workerWaitGroup.Done()
			e.worker(searchCtx, handle, is64Bit, memoryChannel, resultChannel)
		}()
	}

	// Start producer goroutine
	var producerWaitGroup sync.WaitGroup
	producerWaitGroup.Add(1)
	go func() {
		defer producerWaitGroup.Done()
		defer close(memoryChannel) // Close channel when producer is done
		err := e.findMemory(searchCtx, handle, proc.PID, memoryChannel)
		if err != nil {
			log.Err(err).Msg("Failed to find memory regions")
		}
	}()

	// Wait for producer and consumers to complete
	go func() {
		producerWaitGroup.Wait()
		workerWaitGroup.Wait()
		close(resultChannel)
	}()

	// Wait for result
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case result, ok := <-resultChannel:
		if ok && result != "" {
			return result, nil
		}
	}

	return "", errors.ErrNoValidKey
}

// findMemoryV3 searches for writable memory regions in WeChatWin.dll for V3 version
func (e *V3Extractor) findMemory(ctx context.Context, handle windows.Handle, pid uint32, memoryChannel chan<- []byte) error {
	// Find WeChatWin.dll module
	module, isFound := FindModule(pid, V3ModuleName)
	if !isFound {
		return errors.ErrWeChatDLLNotFound
	}
	log.Debug().Msg("Found WeChatWin.dll module at base address: 0x" + fmt.Sprintf("%X", module.ModBaseAddr))

	// Read writable memory regions
	baseAddr := uintptr(module.ModBaseAddr)
	endAddr := baseAddr + uintptr(module.ModBaseSize)
	currentAddr := baseAddr

	for currentAddr < endAddr {
		var mbi windows.MemoryBasicInformation
		err := windows.VirtualQueryEx(handle, currentAddr, &mbi, unsafe.Sizeof(mbi))
		if err != nil {
			break
		}

		// Skip small memory regions
		if mbi.RegionSize < 100*1024 {
			currentAddr += uintptr(mbi.RegionSize)
			continue
		}

		// Check if memory region is writable
		isWritable := (mbi.Protect & (windows.PAGE_READWRITE | windows.PAGE_WRITECOPY | windows.PAGE_EXECUTE_READWRITE | windows.PAGE_EXECUTE_WRITECOPY)) > 0
		if isWritable && uint32(mbi.State) == windows.MEM_COMMIT {
			// Calculate region size, ensure it doesn't exceed DLL bounds
			regionSize := uintptr(mbi.RegionSize)
			if currentAddr+regionSize > endAddr {
				regionSize = endAddr - currentAddr
			}

			// Read writable memory region
			memory := make([]byte, regionSize)
			if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
				select {
				case memoryChannel <- memory:
					log.Debug().Msgf("Memory region: 0x%X - 0x%X, size: %d bytes", currentAddr, currentAddr+regionSize, regionSize)
				case <-ctx.Done():
					return nil
				}
			}
		}

		// Move to next memory region
		currentAddr = uintptr(mbi.BaseAddress) + uintptr(mbi.RegionSize)
	}

	return nil
}

// workerV3 processes memory regions to find V3 version key
func (e *V3Extractor) worker(ctx context.Context, handle windows.Handle, is64Bit bool, memoryChannel <-chan []byte, resultChannel chan<- string) {
	// Define search pattern
	keyPattern := []byte{0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
	ptrSize := 8
	littleEndianFunc := binary.LittleEndian.Uint64

	// Adjust for 32-bit process
	if !is64Bit {
		keyPattern = keyPattern[:4]
		ptrSize = 4
		littleEndianFunc = func(b []byte) uint64 { return uint64(binary.LittleEndian.Uint32(b)) }
	}

	for {
		select {
		case <-ctx.Done():
			return
		case memory, ok := <-memoryChannel:
			if !ok {
				return
			}

			index := len(memory)
			for {
				select {
				case <-ctx.Done():
					return // Exit if context cancelled
				default:
				}

				// Find pattern from end to beginning
				index = bytes.LastIndex(memory[:index], keyPattern)
				if index == -1 || index-ptrSize < 0 {
					break
				}

				// Extract and validate pointer value
				ptrValue := littleEndianFunc(memory[index-ptrSize : index])
				if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
					if key := e.validateKey(handle, ptrValue); key != "" {
						select {
						case resultChannel <- key:
							log.Debug().Msg("Valid key found: " + key)
							return
						default:
						}
					}
				}
				index -= 1 // Continue searching from previous position
			}
		}
	}
}

// validateKey validates a single key candidate
func (e *V3Extractor) validateKey(handle windows.Handle, addr uint64) string {
	keyData := make([]byte, 0x20) // 32-byte key
	if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
		return ""
	}

	// Validate key against database header
	if e.validator.Validate(keyData) {
		return hex.EncodeToString(keyData)
	}

	return ""
}

// FindModule searches for a specified module in the process
func FindModule(pid uint32, name string) (module windows.ModuleEntry32, isFound bool) {
	// Create module snapshot
	snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, pid)
	if err != nil {
		log.Debug().Msgf("Failed to create module snapshot for PID %d: %v", pid, err)
		return module, false
	}
	defer windows.CloseHandle(snapshot)

	// Initialize module entry structure
	module.Size = uint32(windows.SizeofModuleEntry32)

	// Get the first module
	if err := windows.Module32First(snapshot, &module); err != nil {
		log.Debug().Msgf("Module32First failed for PID %d: %v", pid, err)
		return module, false
	}

	// Iterate through all modules to find WeChatWin.dll
	for ; err == nil; err = windows.Module32Next(snapshot, &module) {
		if windows.UTF16ToString(module.Module[:]) == name {
			return module, true
		}
	}
	return module, false
}

package darwin

import (
	"bytes"
	"context"
	"encoding/hex"
	"runtime"
	"sync"

	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt"
	"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

const (
	MaxWorkersV3 = 8
)

var V3KeyPatterns = []KeyPatternInfo{
	{
		Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
		Offsets: []int{24},
	},
}

type V3Extractor struct {
	validator   *decrypt.Validator
	keyPatterns []KeyPatternInfo
}

func NewV3Extractor() *V3Extractor {
	return &V3Extractor{
		keyPatterns: V3KeyPatterns,
	}
}

func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	if proc.Status == model.StatusOffline {
		return "", errors.ErrWeChatOffline
	}

	// Check if SIP is disabled, as it's required for memory reading on macOS
	if !glance.IsSIPDisabled() {
		return "", errors.ErrSIPEnabled
	}

	if e.validator == nil {
		return "", errors.ErrValidatorNotSet
	}

	// Create context to control all goroutines
	searchCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create channels for memory data and results
	memoryChannel := make(chan []byte, 100)
	resultChannel := make(chan string, 1)

	// Determine number of worker goroutines
	workerCount := runtime.NumCPU()
	if workerCount < 2 {
		workerCount = 2
	}
	if workerCount > MaxWorkersV3 {
		workerCount = MaxWorkersV3
	}
	log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)

	// Start consumer goroutines
	var workerWaitGroup sync.WaitGroup
	workerWaitGroup.Add(workerCount)
	for index := 0; index < workerCount; index++ {
		go func() {
			defer workerWaitGroup.Done()
			e.worker(searchCtx, memoryChannel, resultChannel)
		}()
	}

	// Start producer goroutine
	var producerWaitGroup sync.WaitGroup
	producerWaitGroup.Add(1)
	go func() {
		defer producerWaitGroup.Done()
		defer close(memoryChannel) // Close channel when producer is done
		err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
		if err != nil {
			log.Err(err).Msg("Failed to read memory")
		}
	}()

	// Wait for producer and consumers to complete
	go func() {
		producerWaitGroup.Wait()
		workerWaitGroup.Wait()
		close(resultChannel)
	}()

	// Wait for result
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case result, ok := <-resultChannel:
		if ok && result != "" {
			return result, nil
		}
	}

	return "", errors.ErrNoValidKey
}

// findMemory searches for memory regions using Glance
func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel chan<- []byte) error {
	// Initialize a Glance instance to read process memory
	g := glance.NewGlance(pid)

	// Read memory data
	memory, err := g.Read()
	if err != nil {
		return err
	}

	totalSize := len(memory)
	log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)

	// If memory is small enough, process it as a single chunk
	if totalSize <= MinChunkSize {
		select {
		case memoryChannel <- memory:
			log.Debug().Msg("Memory sent as a single chunk for analysis")
		case <-ctx.Done():
			return ctx.Err()
		}
		return nil
	}

	chunkCount := MaxWorkers * ChunkMultiplier

	// Calculate chunk size based on fixed chunk count
	chunkSize := totalSize / chunkCount
	if chunkSize < MinChunkSize {
		// Reduce number of chunks if each would be too small
		chunkCount = totalSize / MinChunkSize
		if chunkCount == 0 {
			chunkCount = 1
		}
		chunkSize = totalSize / chunkCount
	}

	// Process memory in chunks from end to beginning
	for i := chunkCount - 1; i >= 0; i-- {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// Calculate start and end positions for this chunk
			start := i * chunkSize
			end := (i + 1) * chunkSize

			// Ensure the last chunk includes all remaining memory
			if i == chunkCount-1 {
				end = totalSize
			}

			// Add overlap area to catch patterns at chunk boundaries
			if i > 0 {
				start -= ChunkOverlapBytes
				if start < 0 {
					start = 0
				}
			}

			chunk := memory[start:end]

			log.Debug().
				Int("chunk_index", i+1).
				Int("total_chunks", chunkCount).
				Int("chunk_size", len(chunk)).
				Int("start_offset", start).
				Int("end_offset", end).
				Msg("Processing memory chunk")

			select {
			case memoryChannel <- chunk:
			case <-ctx.Done():
				return ctx.Err()
			}
		}
	}
	return nil
}

// worker processes memory regions to find V3 version key
func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
	for {
		select {
		case <-ctx.Done():
			return
		case memory, ok := <-memoryChannel:
			if !ok {
				return
			}

			if key, ok := e.SearchKey(ctx, memory); ok {
				select {
				case resultChannel <- key:
				default:
				}
			}
		}
	}
}

func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
	for _, keyPattern := range e.keyPatterns {
		index := len(memory)

		for {
			select {
			case <-ctx.Done():
				return "", false
			default:
			}

			// Find pattern from end to beginning
			index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
			if index == -1 {
				break // No more matches found
			}

			// Try each offset for this pattern
			for _, offset := range keyPattern.Offsets {
				// Check if we have enough space for the key
				keyOffset := index + offset
				if keyOffset < 0 || keyOffset+32 > len(memory) {
					continue
				}

				// Extract the key data, which is at the offset position and 32 bytes long
				keyData := memory[keyOffset : keyOffset+32]

				// Validate key against database header
				if e.validator.Validate(keyData) {
					log.Debug().
						Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
						Int("offset", offset).
						Str("key", hex.EncodeToString(keyData)).
						Msg("Key found")
					return hex.EncodeToString(keyData), true
				}
			}

			index -= 1
		}
	}

	return "", false
}

func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
	e.validator = validator
}

package darwin

import (
	"bytes"
	"context"
	"encoding/hex"
	"runtime"
	"sync"

	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt"
	"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

const (
	MaxWorkers        = 8
	MinChunkSize      = 1 * 1024 * 1024 // 1MB
	ChunkOverlapBytes = 1024            // Greater than all offsets
	ChunkMultiplier   = 2               // Number of chunks = MaxWorkers * ChunkMultiplier
)

var V4KeyPatterns = []KeyPatternInfo{
	{
		Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
		Offsets: []int{16, -80, 64},
	},
}

type V4Extractor struct {
	validator   *decrypt.Validator
	keyPatterns []KeyPatternInfo
}

func NewV4Extractor() *V4Extractor {
	return &V4Extractor{
		keyPatterns: V4KeyPatterns,
	}
}

func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	if proc.Status == model.StatusOffline {
		return "", errors.ErrWeChatOffline
	}

	// Check if SIP is disabled, as it's required for memory reading on macOS
	if !glance.IsSIPDisabled() {
		return "", errors.ErrSIPEnabled
	}

	if e.validator == nil {
		return "", errors.ErrValidatorNotSet
	}

	// Create context to control all goroutines
	searchCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create channels for memory data and results
	memoryChannel := make(chan []byte, 100)
	resultChannel := make(chan string, 1)

	// Determine number of worker goroutines
	workerCount := runtime.NumCPU()
	if workerCount < 2 {
		workerCount = 2
	}
	if workerCount > MaxWorkers {
		workerCount = MaxWorkers
	}
	log.Debug().Msgf("Starting %d workers for V4 key search", workerCount)

	// Start consumer goroutines
	var workerWaitGroup sync.WaitGroup
	workerWaitGroup.Add(workerCount)
	for index := 0; index < workerCount; index++ {
		go func() {
			defer workerWaitGroup.Done()
			e.worker(searchCtx, memoryChannel, resultChannel)
		}()
	}

	// Start producer goroutine
	var producerWaitGroup sync.WaitGroup
	producerWaitGroup.Add(1)
	go func() {
		defer producerWaitGroup.Done()
		defer close(memoryChannel) // Close channel when producer is done
		err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
		if err != nil {
			log.Err(err).Msg("Failed to read memory")
		}
	}()

	// Wait for producer and consumers to complete
	go func() {
		producerWaitGroup.Wait()
		workerWaitGroup.Wait()
		close(resultChannel)
	}()

	// Wait for result
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case result, ok := <-resultChannel:
		if ok && result != "" {
			return result, nil
		}
	}

	return "", errors.ErrNoValidKey
}

// findMemory searches for memory regions using Glance
func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel chan<- []byte) error {
	// Initialize a Glance instance to read process memory
	g := glance.NewGlance(pid)

	// Read memory data
	memory, err := g.Read()
	if err != nil {
		return err
	}

	totalSize := len(memory)
	log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)

	// If memory is small enough, process it as a single chunk
	if totalSize <= MinChunkSize {
		select {
		case memoryChannel <- memory:
			log.Debug().Msg("Memory sent as a single chunk for analysis")
		case <-ctx.Done():
			return ctx.Err()
		}
		return nil
	}

	chunkCount := MaxWorkers * ChunkMultiplier

	// Calculate chunk size based on fixed chunk count
	chunkSize := totalSize / chunkCount
	if chunkSize < MinChunkSize {
		// Reduce number of chunks if each would be too small
		chunkCount = totalSize / MinChunkSize
		if chunkCount == 0 {
			chunkCount = 1
		}
		chunkSize = totalSize / chunkCount
	}

	// Process memory in chunks from end to beginning
	for i := chunkCount - 1; i >= 0; i-- {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// Calculate start and end positions for this chunk
			start := i * chunkSize
			end := (i + 1) * chunkSize

			// Ensure the last chunk includes all remaining memory
			if i == chunkCount-1 {
				end = totalSize
			}

			// Add overlap area to catch patterns at chunk boundaries
			if i > 0 {
				start -= ChunkOverlapBytes
				if start < 0 {
					start = 0
				}
			}

			chunk := memory[start:end]

			log.Debug().
				Int("chunk_index", i+1).
				Int("total_chunks", chunkCount).
				Int("chunk_size", len(chunk)).
				Int("start_offset", start).
				Int("end_offset", end).
				Msg("Processing memory chunk")

			select {
			case memoryChannel <- chunk:
			case <-ctx.Done():
				return ctx.Err()
			}
		}
	}

	return nil
}

// worker processes memory regions to find V4 version key
func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
	for {
		select {
		case <-ctx.Done():
			return
		case memory, ok := <-memoryChannel:
			if !ok {
				return
			}

			if key, ok := e.SearchKey(ctx, memory); ok {
				select {
				case resultChannel <- key:
				default:
				}
			}
		}
	}
}

func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
	for _, keyPattern := range e.keyPatterns {
		index := len(memory)

		for {
			select {
			case <-ctx.Done():
				return "", false
			default:
			}

			// Find pattern from end to beginning
			index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
			if index == -1 {
				break // No more matches found
			}

			// Try each offset for this pattern
			for _, offset := range keyPattern.Offsets {
				// Check if we have enough space for the key
				keyOffset := index + offset
				if keyOffset < 0 || keyOffset+32 > len(memory) {
					continue
				}

				// Extract the key data, which is at the offset position and 32 bytes long
				keyData := memory[keyOffset : keyOffset+32]

				// Validate key against database header
				if keyData, ok := e.validate(ctx, keyData); ok {
					log.Debug().
						Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
						Int("offset", offset).
						Str("key", hex.EncodeToString(keyData)).
						Msg("Key found")
					return hex.EncodeToString(keyData), true
				}
			}

			index -= 1
		}
	}

	return "", false
}

func (e *V4Extractor) validate(ctx context.Context, keyDate []byte) ([]byte, bool) {
	if e.validator.Validate(keyDate) {
		return keyDate, true
	}
	// Try to find a valid key by ***
	return nil, false
}

func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
	e.validator = validator
}

type KeyPatternInfo struct {
	Pattern []byte
	Offsets []int
}

package glance

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/sjzar/chatlog/internal/errors"
)

// FIXME 按照 region 读取效率较低，512MB 内存读取耗时约 18s

type Glance struct {
	PID        uint32
	MemRegions []MemRegion
	pipePath   string
	data       []byte
}

func NewGlance(pid uint32) *Glance {
	return &Glance{
		PID:      pid,
		pipePath: filepath.Join(os.TempDir(), fmt.Sprintf("chatlog_pipe_%d", time.Now().UnixNano())),
	}
}

func (g *Glance) Read() ([]byte, error) {
	if g.data != nil {
		return g.data, nil
	}

	regions, err := GetVmmap(g.PID)
	if err != nil {
		return nil, err
	}
	g.MemRegions = MemRegionsFilter(regions)

	if len(g.MemRegions) == 0 {
		return nil, errors.ErrNoMemoryRegionsFound
	}

	region := g.MemRegions[0]

	// 1. Create pipe file
	if err := exec.Command("mkfifo", g.pipePath).Run(); err != nil {
		return nil, errors.CreatePipeFileFailed(err)
	}
	defer os.Remove(g.pipePath)

	// Start a goroutine to read from the pipe
	dataCh := make(chan []byte, 1)
	errCh := make(chan error, 1)
	go func() {
		// Open pipe for reading
		file, err := os.OpenFile(g.pipePath, os.O_RDONLY, 0600)
		if err != nil {
			errCh <- errors.OpenPipeFileFailed(err)
			return
		}
		defer file.Close()

		// Read all data from pipe
		data, err := io.ReadAll(file)
		if err != nil {
			errCh <- errors.ReadPipeFileFailed(err)
			return
		}
		dataCh <- data
	}()

	// 2 & 3. Execute lldb command to read memory directly with all parameters
	size := region.End - region.Start
	lldbCmd := fmt.Sprintf("lldb -p %d -o \"memory read --binary --force --outfile %s --count %d 0x%x\" -o \"quit\"",
		g.PID, g.pipePath, size, region.Start)

	cmd := exec.Command("bash", "-c", lldbCmd)

	// Set up stdout pipe for monitoring (optional)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	// Start the command
	if err := cmd.Start(); err != nil {
		return nil, errors.RunCmdFailed(err)
	}

	// Monitor lldb output (optional)
	go func() {
		scanner := bufio.NewScanner(stdout)
		for scanner.Scan() {
			// Uncomment for debugging:
			// fmt.Println(scanner.Text())
		}
	}()

	// Wait for data with timeout
	select {
	case data := <-dataCh:
		g.data = data
	case err := <-errCh:
		return nil, errors.ReadMemoryFailed(err)
	case <-time.After(30 * time.Second):
		cmd.Process.Kill()
		return nil, errors.ErrReadMemoryTimeout
	}

	// Wait for the command to finish
	if err := cmd.Wait(); err != nil {
		// We already have the data, so just log the error
		log.Err(err).Msg("lldb process exited with error")
	}

	return g.data, nil
}

package windows

import (
	"context"
	"crypto/sha512"
	"encoding/hex"
	"hash"
	"io"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/common"

	"golang.org/x/crypto/pbkdf2"
)

// V4 版本特定常量
const (
	V4IterCount    = 256000
	HmacSHA512Size = 64
)

// V4Decryptor 实现Windows V4版本的解密器
type V4Decryptor struct {
	// V4 特定参数
	iterCount int
	hmacSize  int
	hashFunc  func() hash.Hash
	reserve   int
	pageSize  int
	version   string
}

// NewV4Decryptor 创建Windows V4解密器
func NewV4Decryptor() *V4Decryptor {
	hashFunc := sha512.New
	hmacSize := HmacSHA512Size
	reserve := common.IVSize + hmacSize
	if reserve%common.AESBlockSize != 0 {
		reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
	}

	return &V4Decryptor{
		iterCount: V4IterCount,
		hmacSize:  hmacSize,
		hashFunc:  hashFunc,
		reserve:   reserve,
		pageSize:  PageSize,
		version:   "Windows v4",
	}
}

// deriveKeys 派生加密密钥和MAC密钥
func (d *V4Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
	// 生成加密密钥
	encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)

	// 生成MAC密钥
	macSalt := common.XorBytes(salt, 0x3a)
	macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)

	return encKey, macKey
}

// Validate 验证密钥是否有效
func (d *V4Decryptor) Validate(page1 []byte, key []byte) bool {
	if len(page1) < d.pageSize || len(key) != common.KeySize {
		return false
	}

	salt := page1[:common.SaltSize]
	return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}

// Decrypt 解密数据库
func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
	// 解码密钥
	key, err := hex.DecodeString(hexKey)
	if err != nil {
		return errors.DecodeKeyFailed(err)
	}

	// 打开数据库文件并读取基本信息
	dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
	if err != nil {
		return err
	}

	// 验证密钥
	if !d.Validate(dbInfo.FirstPage, key) {
		return errors.ErrDecryptIncorrectKey
	}

	// 计算密钥
	encKey, macKey := d.deriveKeys(key, dbInfo.Salt)

	// 打开数据库文件
	dbFile, err := os.Open(dbfile)
	if err != nil {
		return errors.OpenFileFailed(dbfile, err)
	}
	defer dbFile.Close()

	// 写入SQLite头
	_, err = output.Write([]byte(common.SQLiteHeader))
	if err != nil {
		return errors.WriteOutputFailed(err)
	}

	// 处理每一页
	pageBuf := make([]byte, d.pageSize)

	for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
		// 检查是否取消
		select {
		case <-ctx.Done():
			return errors.ErrDecryptOperationCanceled
		default:
			// 继续处理
		}

		// 读取一页
		n, err := io.ReadFull(dbFile, pageBuf)
		if err != nil {
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				// 处理最后一部分页面
				if n > 0 {
					break
				}
			}
			return errors.ReadFileFailed(dbfile, err)
		}

		// 检查页面是否全为零
		allZeros := true
		for _, b := range pageBuf {
			if b != 0 {
				allZeros = false
				break
			}
		}

		if allZeros {
			// 写入零页面
			_, err = output.Write(pageBuf)
			if err != nil {
				return errors.WriteOutputFailed(err)
			}
			continue
		}

		// 解密页面
		decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
		if err != nil {
			return err
		}

		// 写入解密后的页面
		_, err = output.Write(decryptedData)
		if err != nil {
			return errors.WriteOutputFailed(err)
		}
	}

	return nil
}

// GetPageSize 返回页面大小
func (d *V4Decryptor) GetPageSize() int {
	return d.pageSize
}

// GetReserve 返回保留字节数
func (d *V4Decryptor) GetReserve() int {
	return d.reserve
}

// GetHMACSize 返回HMAC大小
func (d *V4Decryptor) GetHMACSize() int {
	return d.hmacSize
}

// GetVersion 返回解密器版本
func (d *V4Decryptor) GetVersion() string {
	return d.version
}

// GetIterCount 返回迭代次数（Windows特有）
func (d *V4Decryptor) GetIterCount() int {
	return d.iterCount
}

package windows

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"hash"
	"io"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/common"

	"golang.org/x/crypto/pbkdf2"
)

// V3 版本特定常量
const (
	PageSize     = 4096
	V3IterCount  = 64000
	HmacSHA1Size = 20
)

// V3Decryptor 实现Windows V3版本的解密器
type V3Decryptor struct {
	// V3 特定参数
	iterCount int
	hmacSize  int
	hashFunc  func() hash.Hash
	reserve   int
	pageSize  int
	version   string
}

// NewV3Decryptor 创建Windows V3解密器
func NewV3Decryptor() *V3Decryptor {
	hashFunc := sha1.New
	hmacSize := HmacSHA1Size
	reserve := common.IVSize + hmacSize
	if reserve%common.AESBlockSize != 0 {
		reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
	}

	return &V3Decryptor{
		iterCount: V3IterCount,
		hmacSize:  hmacSize,
		hashFunc:  hashFunc,
		reserve:   reserve,
		pageSize:  PageSize,
		version:   "Windows v3",
	}
}

// deriveKeys 派生加密密钥和MAC密钥
func (d *V3Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
	// 生成加密密钥
	encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)

	// 生成MAC密钥
	macSalt := common.XorBytes(salt, 0x3a)
	macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)

	return encKey, macKey
}

// Validate 验证密钥是否有效
func (d *V3Decryptor) Validate(page1 []byte, key []byte) bool {
	if len(page1) < d.pageSize || len(key) != common.KeySize {
		return false
	}

	salt := page1[:common.SaltSize]
	return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}

// Decrypt 解密数据库
func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
	// 解码密钥
	key, err := hex.DecodeString(hexKey)
	if err != nil {
		return errors.DecodeKeyFailed(err)
	}

	// 打开数据库文件并读取基本信息
	dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
	if err != nil {
		return err
	}

	// 验证密钥
	if !d.Validate(dbInfo.FirstPage, key) {
		return errors.ErrDecryptIncorrectKey
	}

	// 计算密钥
	encKey, macKey := d.deriveKeys(key, dbInfo.Salt)

	// 打开数据库文件
	dbFile, err := os.Open(dbfile)
	if err != nil {
		return errors.OpenFileFailed(dbfile, err)
	}
	defer dbFile.Close()

	// 写入SQLite头
	_, err = output.Write([]byte(common.SQLiteHeader))
	if err != nil {
		return errors.WriteOutputFailed(err)
	}

	// 处理每一页
	pageBuf := make([]byte, d.pageSize)

	for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
		// 检查是否取消
		select {
		case <-ctx.Done():
			return errors.ErrDecryptOperationCanceled
		default:
			// 继续处理
		}

		// 读取一页
		n, err := io.ReadFull(dbFile, pageBuf)
		if err != nil {
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				// 处理最后一部分页面
				if n > 0 {
					break
				}
			}
			return errors.ReadFileFailed(dbfile, err)
		}

		// 检查页面是否全为零
		allZeros := true
		for _, b := range pageBuf {
			if b != 0 {
				allZeros = false
				break
			}
		}

		if allZeros {
			// 写入零页面
			_, err = output.Write(pageBuf)
			if err != nil {
				return errors.WriteOutputFailed(err)
			}
			continue
		}

		// 解密页面
		decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
		if err != nil {
			return err
		}

		// 写入解密后的页面
		_, err = output.Write(decryptedData)
		if err != nil {
			return errors.WriteOutputFailed(err)
		}
	}

	return nil
}

// GetPageSize 返回页面大小
func (d *V3Decryptor) GetPageSize() int {
	return d.pageSize
}

// GetReserve 返回保留字节数
func (d *V3Decryptor) GetReserve() int {
	return d.reserve
}

// GetHMACSize 返回HMAC大小
func (d *V3Decryptor) GetHMACSize() int {
	return d.hmacSize
}

// GetVersion 返回解密器版本
func (d *V3Decryptor) GetVersion() string {
	return d.version
}

// GetIterCount 返回迭代次数（Windows特有）
func (d *V3Decryptor) GetIterCount() int {
	return d.iterCount
}

package darwin

import (
	"context"
	"crypto/sha512"
	"encoding/hex"
	"hash"
	"io"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/common"

	"golang.org/x/crypto/pbkdf2"
)

// Darwin Version 4 same as WIndows Version 4

// V4 版本特定常量
const (
	V4PageSize     = 4096
	V4IterCount    = 256000
	HmacSHA512Size = 64
)

// V4Decryptor 实现Windows V4版本的解密器
type V4Decryptor struct {
	// V4 特定参数
	iterCount int
	hmacSize  int
	hashFunc  func() hash.Hash
	reserve   int
	pageSize  int
	version   string
}

// NewV4Decryptor 创建Windows V4解密器
func NewV4Decryptor() *V4Decryptor {
	hashFunc := sha512.New
	hmacSize := HmacSHA512Size
	reserve := common.IVSize + hmacSize
	if reserve%common.AESBlockSize != 0 {
		reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
	}

	return &V4Decryptor{
		iterCount: V4IterCount,
		hmacSize:  hmacSize,
		hashFunc:  hashFunc,
		reserve:   reserve,
		pageSize:  V4PageSize,
		version:   "macOS v4",
	}
}

// deriveKeys 派生加密密钥和MAC密钥
func (d *V4Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
	// 生成加密密钥
	encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)

	// 生成MAC密钥
	macSalt := common.XorBytes(salt, 0x3a)
	macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)

	return encKey, macKey
}

// Validate 验证密钥是否有效
func (d *V4Decryptor) Validate(page1 []byte, key []byte) bool {
	if len(page1) < d.pageSize || len(key) != common.KeySize {
		return false
	}

	salt := page1[:common.SaltSize]
	return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}

// Decrypt 解密数据库
func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
	// 解码密钥
	key, err := hex.DecodeString(hexKey)
	if err != nil {
		return errors.DecodeKeyFailed(err)
	}

	// 打开数据库文件并读取基本信息
	dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
	if err != nil {
		return err
	}

	// 验证密钥
	if !d.Validate(dbInfo.FirstPage, key) {
		return errors.ErrDecryptIncorrectKey
	}

	// 计算密钥
	encKey, macKey := d.deriveKeys(key, dbInfo.Salt)

	// 打开数据库文件
	dbFile, err := os.Open(dbfile)
	if err != nil {
		return errors.OpenFileFailed(dbfile, err)
	}
	defer dbFile.Close()

	// 写入SQLite头
	_, err = output.Write([]byte(common.SQLiteHeader))
	if err != nil {
		return errors.WriteOutputFailed(err)
	}

	// 处理每一页
	pageBuf := make([]byte, d.pageSize)

	for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
		// 检查是否取消
		select {
		case <-ctx.Done():
			return errors.ErrDecryptOperationCanceled
		default:
			// 继续处理
		}

		// 读取一页
		n, err := io.ReadFull(dbFile, pageBuf)
		if err != nil {
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				// 处理最后一部分页面
				if n > 0 {
					break
				}
			}
			return errors.ReadFileFailed(dbfile, err)
		}

		// 检查页面是否全为零
		allZeros := true
		for _, b := range pageBuf {
			if b != 0 {
				allZeros = false
				break
			}
		}

		if allZeros {
			// 写入零页面
			_, err = output.Write(pageBuf)
			if err != nil {
				return errors.WriteOutputFailed(err)
			}
			continue
		}

		// 解密页面
		decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
		if err != nil {
			return err
		}

		// 写入解密后的页面
		_, err = output.Write(decryptedData)
		if err != nil {
			return errors.WriteOutputFailed(err)
		}
	}

	return nil
}

// GetPageSize 返回页面大小
func (d *V4Decryptor) GetPageSize() int {
	return d.pageSize
}

// GetReserve 返回保留字节数
func (d *V4Decryptor) GetReserve() int {
	return d.reserve
}

// GetHMACSize 返回HMAC大小
func (d *V4Decryptor) GetHMACSize() int {
	return d.hmacSize
}

// GetVersion 返回解密器版本
func (d *V4Decryptor) GetVersion() string {
	return d.version
}

// GetIterCount 返回迭代次数（Windows特有）
func (d *V4Decryptor) GetIterCount() int {
	return d.iterCount
}

package darwin

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"hash"
	"io"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/common"

	"golang.org/x/crypto/pbkdf2"
)

// 常量定义
const (
	V3PageSize   = 1024
	HmacSHA1Size = 20
)

// V3Decryptor 实现 macOS V3 版本的解密器
type V3Decryptor struct {
	// macOS V3 特定参数
	hmacSize int
	hashFunc func() hash.Hash
	reserve  int
	pageSize int
	version  string
}

// NewV3Decryptor 创建 macOS V3 解密器
func NewV3Decryptor() *V3Decryptor {
	hashFunc := sha1.New
	hmacSize := HmacSHA1Size
	reserve := common.IVSize + hmacSize
	if reserve%common.AESBlockSize != 0 {
		reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
	}

	return &V3Decryptor{
		hmacSize: hmacSize,
		hashFunc: hashFunc,
		reserve:  reserve,
		pageSize: V3PageSize,
		version:  "macOS v3",
	}
}

// deriveKeys 派生 MAC 密钥
// 注意：macOS V3 版本直接使用提供的密钥作为加密密钥，不进行 PBKDF2 派生
func (d *V3Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
	// 对于 macOS V3，直接使用密钥作为加密密钥
	encKey := key

	// 生成 MAC 密钥
	macSalt := common.XorBytes(salt, 0x3a)
	macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)

	return encKey, macKey
}

// Validate 验证密钥是否有效
func (d *V3Decryptor) Validate(page1 []byte, key []byte) bool {
	if len(page1) < d.pageSize || len(key) != common.KeySize {
		return false
	}

	salt := page1[:common.SaltSize]
	return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}

// Decrypt 解密数据库
func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
	// 解码密钥
	key, err := hex.DecodeString(hexKey)
	if err != nil {
		return errors.DecodeKeyFailed(err)
	}

	// 打开数据库文件并读取基本信息
	dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
	if err != nil {
		return err
	}

	// 验证密钥
	if !d.Validate(dbInfo.FirstPage, key) {
		return errors.ErrDecryptIncorrectKey
	}

	// 计算密钥
	encKey, macKey := d.deriveKeys(key, dbInfo.Salt)

	// 打开数据库文件
	dbFile, err := os.Open(dbfile)
	if err != nil {
		return errors.OpenFileFailed(dbfile, err)
	}
	defer dbFile.Close()

	// 写入 SQLite 头
	_, err = output.Write([]byte(common.SQLiteHeader))
	if err != nil {
		return errors.WriteOutputFailed(err)
	}

	// 处理每一页
	pageBuf := make([]byte, d.pageSize)

	for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
		// 检查是否取消
		select {
		case <-ctx.Done():
			return errors.ErrDecryptOperationCanceled
		default:
			// 继续处理
		}

		// 读取一页
		n, err := io.ReadFull(dbFile, pageBuf)
		if err != nil {
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				// 处理最后一部分页面
				if n > 0 {
					break
				}
			}
			return errors.ReadFileFailed(dbfile, err)
		}

		// 检查页面是否全为零
		allZeros := true
		for _, b := range pageBuf {
			if b != 0 {
				allZeros = false
				break
			}
		}

		if allZeros {
			// 写入零页面
			_, err = output.Write(pageBuf)
			if err != nil {
				return errors.WriteOutputFailed(err)
			}
			continue
		}

		// 解密页面
		decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
		if err != nil {
			return err
		}

		// 写入解密后的页面
		_, err = output.Write(decryptedData)
		if err != nil {
			return errors.WriteOutputFailed(err)
		}
	}

	return nil
}

// GetPageSize 返回页面大小
func (d *V3Decryptor) GetPageSize() int {
	return d.pageSize
}

// GetReserve 返回保留字节数
func (d *V3Decryptor) GetReserve() int {
	return d.reserve
}

// GetHMACSize 返回HMAC大小
func (d *V3Decryptor) GetHMACSize() int {
	return d.hmacSize
}

// GetVersion 返回解密器版本
func (d *V3Decryptor) GetVersion() string {
	return d.version
}

package glance

import (
	"bufio"
	"fmt"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/sjzar/chatlog/internal/errors"
)

const (
	FilterRegionType = "MALLOC_NANO"
	FilterSHRMOD     = "SM=PRV"
	CommandVmmap     = "vmmap"
)

type MemRegion struct {
	RegionType   string
	Start        uint64
	End          uint64
	VSize        uint64 // Size in bytes
	RSDNT        uint64 // Resident memory size in bytes (new field)
	SHRMOD       string
	Permissions  string
	RegionDetail string
}

func GetVmmap(pid uint32) ([]MemRegion, error) {
	// Execute vmmap command
	cmd := exec.Command(CommandVmmap, "-wide", fmt.Sprintf("%d", pid))
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, errors.RunCmdFailed(err)
	}

	// Parse the output using the existing LoadVmmap function
	return LoadVmmap(string(output))
}

func LoadVmmap(output string) ([]MemRegion, error) {
	var regions []MemRegion

	scanner := bufio.NewScanner(strings.NewReader(output))

	// Skip lines until we find the header
	foundHeader := false
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "==== Writable regions for") {
			foundHeader = true
			// Skip the column headers line
			scanner.Scan()
			break
		}
	}

	if !foundHeader {
		return nil, nil // No vmmap data found
	}

	// Regular expression to parse the vmmap output lines
	// Format: REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
	// Updated regex to capture RSDNT value (second value in brackets)
	re := regexp.MustCompile(`^(\S+)\s+([0-9a-f]+)-([0-9a-f]+)\s+\[\s*(\S+)\s+(\S+)(?:\s+\S+){2}\]\s+(\S+)\s+(\S+)(?:\s+\S+)?\s+(.*)$`)

	// Parse each line
	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			continue
		}

		matches := re.FindStringSubmatch(line)
		if len(matches) >= 9 { // Updated to check for at least 9 matches

			// Parse start and end addresses
			start, _ := strconv.ParseUint(matches[2], 16, 64)
			end, _ := strconv.ParseUint(matches[3], 16, 64)

			// Parse VSize as numeric value
			vsize := parseSize(matches[4])

			// Parse RSDNT as numeric value (new)
			rsdnt := parseSize(matches[5])

			region := MemRegion{
				RegionType:   strings.TrimSpace(matches[1]),
				Start:        start,
				End:          end,
				VSize:        vsize,
				RSDNT:        rsdnt,                         // Add the new RSDNT field
				Permissions:  matches[6],                    // Shifted index
				SHRMOD:       matches[7],                    // Shifted index
				RegionDetail: strings.TrimSpace(matches[8]), // Shifted index
			}

			regions = append(regions, region)
		}
	}

	return regions, nil
}

func MemRegionsFilter(regions []MemRegion) []MemRegion {
	var filteredRegions []MemRegion
	for _, region := range regions {
		if region.RegionType == FilterRegionType {
			filteredRegions = append(filteredRegions, region)
		}
	}
	return filteredRegions
}

// parseSize converts size strings like "5616K" or "128.0M" to bytes (uint64)
func parseSize(sizeStr string) uint64 {
	// Remove any whitespace
	sizeStr = strings.TrimSpace(sizeStr)

	// Define multipliers for different units
	multipliers := map[string]uint64{
		"B":  1,
		"K":  1024,
		"KB": 1024,
		"M":  1024 * 1024,
		"MB": 1024 * 1024,
		"G":  1024 * 1024 * 1024,
		"GB": 1024 * 1024 * 1024,
	}

	// Regular expression to match numbers with optional decimal point and unit
	// This will match formats like: "5616K", "128.0M", "1.5G", etc.
	re := regexp.MustCompile(`^(\d+(?:\.\d+)?)([KMGB]+)?$`)
	matches := re.FindStringSubmatch(sizeStr)

	if len(matches) < 2 {
		return 0 // No match found
	}

	// Parse the numeric part (which may include a decimal point)
	numStr := matches[1]
	numVal, err := strconv.ParseFloat(numStr, 64)
	if err != nil {
		return 0
	}

	// Determine the multiplier based on the unit
	multiplier := uint64(1) // Default if no unit specified
	if len(matches) >= 3 && matches[2] != "" {
		unit := matches[2]
		if m, ok := multipliers[unit]; ok {
			multiplier = m
		}
	}

	// Calculate final size in bytes (rounding to nearest integer)
	return uint64(numVal*float64(multiplier) + 0.5)
}

package v4

import (
	"context"
	"crypto/md5"
	"database/sql"
	"encoding/hex"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/fsnotify/fsnotify"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource/dbm"
	"github.com/sjzar/chatlog/pkg/util"
)

const (
	Message = "message"
	Contact = "contact"
	Session = "session"
	Media   = "media"
	Voice   = "voice"
)

var Groups = []*dbm.Group{
	{
		Name:      Message,
		Pattern:   `^message_([0-9]?[0-9])?\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Contact,
		Pattern:   `^contact\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Session,
		Pattern:   `session\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Media,
		Pattern:   `^hardlink\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Voice,
		Pattern:   `^media_([0-9]?[0-9])?\.db$`,
		BlackList: []string{},
	},
}

// MessageDBInfo 存储消息数据库的信息
type MessageDBInfo struct {
	FilePath  string
	StartTime time.Time
	EndTime   time.Time
}

type DataSource struct {
	path string
	dbm  *dbm.DBManager

	// 消息数据库信息
	messageInfos []MessageDBInfo
}

func New(path string) (*DataSource, error) {

	ds := &DataSource{
		path:         path,
		dbm:          dbm.NewDBManager(path),
		messageInfos: make([]MessageDBInfo, 0),
	}

	for _, g := range Groups {
		ds.dbm.AddGroup(g)
	}

	if err := ds.dbm.Start(); err != nil {
		return nil, err
	}

	if err := ds.initMessageDbs(); err != nil {
		return nil, errors.DBInitFailed(err)
	}

	ds.dbm.AddCallback(Message, func(event fsnotify.Event) error {
		if !event.Op.Has(fsnotify.Create) {
			return nil
		}
		if err := ds.initMessageDbs(); err != nil {
			log.Err(err).Msgf("Failed to reinitialize message DBs: %s", event.Name)
		}
		return nil
	})

	return ds, nil
}

func (ds *DataSource) SetCallback(name string, callback func(event fsnotify.Event) error) error {
	if name == "chatroom" {
		name = Contact
	}
	return ds.dbm.AddCallback(name, callback)
}

func (ds *DataSource) initMessageDbs() error {
	dbPaths, err := ds.dbm.GetDBPath(Message)
	if err != nil {
		if strings.Contains(err.Error(), "db file not found") {
			ds.messageInfos = make([]MessageDBInfo, 0)
			return nil
		}
		return err
	}

	// 处理每个数据库文件
	infos := make([]MessageDBInfo, 0)
	for _, filePath := range dbPaths {
		db, err := ds.dbm.OpenDB(filePath)
		if err != nil {
			log.Err(err).Msgf("获取数据库 %s 失败", filePath)
			continue
		}

		// 获取 Timestamp 表中的开始时间
		var startTime time.Time
		var timestamp int64

		row := db.QueryRow("SELECT timestamp FROM Timestamp LIMIT 1")
		if err := row.Scan(&timestamp); err != nil {
			log.Err(err).Msgf("获取数据库 %s 的时间戳失败", filePath)
			continue
		}
		startTime = time.Unix(timestamp, 0)

		// 保存数据库信息
		infos = append(infos, MessageDBInfo{
			FilePath:  filePath,
			StartTime: startTime,
		})
	}

	// 按照 StartTime 排序数据库文件
	sort.Slice(infos, func(i, j int) bool {
		return infos[i].StartTime.Before(infos[j].StartTime)
	})

	// 设置结束时间
	for i := range infos {
		if i == len(infos)-1 {
			infos[i].EndTime = time.Now()
		} else {
			infos[i].EndTime = infos[i+1].StartTime
		}
	}
	ds.messageInfos = infos
	return nil
}

// getDBInfosForTimeRange 获取时间范围内的数据库信息
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
	var dbs []MessageDBInfo
	for _, info := range ds.messageInfos {
		if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
			dbs = append(dbs, info)
		}
	}
	return dbs
}

func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
	if talker == "" {
		return nil, errors.ErrTalkerEmpty
	}

	// 解析talker参数，支持多个talker（以英文逗号分隔）
	talkers := util.Str2List(talker, ",")
	if len(talkers) == 0 {
		return nil, errors.ErrTalkerEmpty
	}

	// 找到时间范围内的数据库文件
	dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
	if len(dbInfos) == 0 {
		return nil, errors.TimeRangeNotFound(startTime, endTime)
	}

	// 解析sender参数，支持多个发送者（以英文逗号分隔）
	senders := util.Str2List(sender, ",")

	// 预编译正则表达式（如果有keyword）
	var regex *regexp.Regexp
	if keyword != "" {
		var err error
		regex, err = regexp.Compile(keyword)
		if err != nil {
			return nil, errors.QueryFailed("invalid regex pattern", err)
		}
	}

	// 从每个相关数据库中查询消息，并在读取时进行过滤
	filteredMessages := []*model.Message{}

	for _, dbInfo := range dbInfos {
		// 检查上下文是否已取消
		if err := ctx.Err(); err != nil {
			return nil, err
		}

		db, err := ds.dbm.OpenDB(dbInfo.FilePath)
		if err != nil {
			log.Error().Msgf("数据库 %s 未打开", dbInfo.FilePath)
			continue
		}

		// 对每个talker进行查询
		for _, talkerItem := range talkers {
			// 构建表名
			_talkerMd5Bytes := md5.Sum([]byte(talkerItem))
			talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
			tableName := "Msg_" + talkerMd5

			// 检查表是否存在
			var exists bool
			err = db.QueryRowContext(ctx,
				"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
				tableName).Scan(&exists)

			if err != nil {
				if err == sql.ErrNoRows {
					// 表不存在，继续下一个talker
					continue
				}
				return nil, errors.QueryFailed("", err)
			}

			// 构建查询条件
			conditions := []string{"create_time >= ? AND create_time <= ?"}
			args := []interface{}{startTime.Unix(), endTime.Unix()}
			log.Debug().Msgf("Table name: %s", tableName)
			log.Debug().Msgf("Start time: %d, End time: %d", startTime.Unix(), endTime.Unix())

			query := fmt.Sprintf(`
				SELECT m.sort_seq, m.server_id, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
				FROM %s m
				LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid
				WHERE %s
				ORDER BY m.sort_seq ASC
			`, tableName, strings.Join(conditions, " AND "))

			// 执行查询
			rows, err := db.QueryContext(ctx, query, args...)
			if err != nil {
				// 如果表不存在，SQLite 会返回错误
				if strings.Contains(err.Error(), "no such table") {
					continue
				}
				log.Err(err).Msgf("从数据库 %s 查询消息失败", dbInfo.FilePath)
				continue
			}

			// 处理查询结果，在读取时进行过滤
			for rows.Next() {
				var msg model.MessageV4
				err := rows.Scan(
					&msg.SortSeq,
					&msg.ServerID,
					&msg.LocalType,
					&msg.UserName,
					&msg.CreateTime,
					&msg.MessageContent,
					&msg.PackedInfoData,
					&msg.Status,
				)
				if err != nil {
					rows.Close()
					return nil, errors.ScanRowFailed(err)
				}

				// 将消息转换为标准格式
				message := msg.Wrap(talkerItem)

				// 应用sender过滤
				if len(senders) > 0 {
					senderMatch := false
					for _, s := range senders {
						if message.Sender == s {
							senderMatch = true
							break
						}
					}
					if !senderMatch {
						continue // 不匹配sender，跳过此消息
					}
				}

				// 应用keyword过滤
				if regex != nil {
					plainText := message.PlainTextContent()
					if !regex.MatchString(plainText) {
						continue // 不匹配keyword，跳过此消息
					}
				}

				// 通过所有过滤条件，保留此消息
				filteredMessages = append(filteredMessages, message)

				// 检查是否已经满足分页处理数量
				if limit > 0 && len(filteredMessages) >= offset+limit {
					// 已经获取了足够的消息，可以提前返回
					rows.Close()

					// 对所有消息按时间排序
					sort.Slice(filteredMessages, func(i, j int) bool {
						return filteredMessages[i].Seq < filteredMessages[j].Seq
					})

					// 处理分页
					if offset >= len(filteredMessages) {
						return []*model.Message{}, nil
					}
					end := offset + limit
					if end > len(filteredMessages) {
						end = len(filteredMessages)
					}
					return filteredMessages[offset:end], nil
				}
			}
			rows.Close()
		}
	}

	// 对所有消息按时间排序
	sort.Slice(filteredMessages, func(i, j int) bool {
		return filteredMessages[i].Seq < filteredMessages[j].Seq
	})

	// 处理分页
	if limit > 0 {
		if offset >= len(filteredMessages) {
			return []*model.Message{}, nil
		}
		end := offset + limit
		if end > len(filteredMessages) {
			end = len(filteredMessages)
		}
		return filteredMessages[offset:end], nil
	}

	return filteredMessages, nil
}

// 联系人
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT username, local_type, alias, remark, nick_name
				FROM contact
				WHERE username = ? OR alias = ? OR remark = ? OR nick_name = ?`
		args = []interface{}{key, key, key, key}
	} else {
		// 查询所有联系人
		query = `SELECT username, local_type, alias, remark, nick_name FROM contact`
	}

	// 添加排序、分页
	query += ` ORDER BY username`
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Contact)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	contacts := []*model.Contact{}
	for rows.Next() {
		var contactV4 model.ContactV4
		err := rows.Scan(
			&contactV4.UserName,
			&contactV4.LocalType,
			&contactV4.Alias,
			&contactV4.Remark,
			&contactV4.NickName,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		contacts = append(contacts, contactV4.Wrap())
	}

	return contacts, nil
}

// 群聊
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
	var query string
	var args []interface{}

	// 执行查询
	db, err := ds.dbm.GetDB(Contact)
	if err != nil {
		return nil, err
	}

	if key != "" {
		// 按照关键字查询
		query = `SELECT username, owner, ext_buffer FROM chat_room WHERE username = ?`
		args = []interface{}{key}

		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		chatRooms := []*model.ChatRoom{}
		for rows.Next() {
			var chatRoomV4 model.ChatRoomV4
			err := rows.Scan(
				&chatRoomV4.UserName,
				&chatRoomV4.Owner,
				&chatRoomV4.ExtBuffer,
			)

			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}

			chatRooms = append(chatRooms, chatRoomV4.Wrap())
		}

		// 如果没有找到群聊，尝试通过联系人查找
		if len(chatRooms) == 0 {
			contacts, err := ds.GetContacts(ctx, key, 1, 0)
			if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
				// 再次尝试通过用户名查找群聊
				rows, err := db.QueryContext(ctx,
					`SELECT username, owner, ext_buffer FROM chat_room WHERE username = ?`,
					contacts[0].UserName)

				if err != nil {
					return nil, errors.QueryFailed(query, err)
				}
				defer rows.Close()

				for rows.Next() {
					var chatRoomV4 model.ChatRoomV4
					err := rows.Scan(
						&chatRoomV4.UserName,
						&chatRoomV4.Owner,
						&chatRoomV4.ExtBuffer,
					)

					if err != nil {
						return nil, errors.ScanRowFailed(err)
					}

					chatRooms = append(chatRooms, chatRoomV4.Wrap())
				}

				// 如果群聊记录不存在，但联系人记录存在，创建一个模拟的群聊对象
				if len(chatRooms) == 0 {
					chatRooms = append(chatRooms, &model.ChatRoom{
						Name:             contacts[0].UserName,
						Users:            make([]model.ChatRoomUser, 0),
						User2DisplayName: make(map[string]string),
					})
				}
			}
		}

		return chatRooms, nil
	} else {
		// 查询所有群聊
		query = `SELECT username, owner, ext_buffer FROM chat_room`

		// 添加排序、分页
		query += ` ORDER BY username`
		if limit > 0 {
			query += fmt.Sprintf(" LIMIT %d", limit)
			if offset > 0 {
				query += fmt.Sprintf(" OFFSET %d", offset)
			}
		}

		// 执行查询
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		chatRooms := []*model.ChatRoom{}
		for rows.Next() {
			var chatRoomV4 model.ChatRoomV4
			err := rows.Scan(
				&chatRoomV4.UserName,
				&chatRoomV4.Owner,
				&chatRoomV4.ExtBuffer,
			)

			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}

			chatRooms = append(chatRooms, chatRoomV4.Wrap())
		}

		return chatRooms, nil
	}
}

// 最近会话
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT username, summary, last_timestamp, last_msg_sender, last_sender_display_name
				FROM SessionTable
				WHERE username = ? OR last_sender_display_name = ?
				ORDER BY sort_timestamp DESC`
		args = []interface{}{key, key}
	} else {
		// 查询所有会话
		query = `SELECT username, summary, last_timestamp, last_msg_sender, last_sender_display_name
				FROM SessionTable
				ORDER BY sort_timestamp DESC`
	}

	// 添加分页
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Session)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	sessions := []*model.Session{}
	for rows.Next() {
		var sessionV4 model.SessionV4
		err := rows.Scan(
			&sessionV4.Username,
			&sessionV4.Summary,
			&sessionV4.LastTimestamp,
			&sessionV4.LastMsgSender,
			&sessionV4.LastSenderDisplayName,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		sessions = append(sessions, sessionV4.Wrap())
	}

	return sessions, nil
}

func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
	if key == "" {
		return nil, errors.ErrKeyEmpty
	}

	var table string
	switch _type {
	case "image":
		table = "image_hardlink_info_v3"
	case "video":
		table = "video_hardlink_info_v3"
	case "file":
		table = "file_hardlink_info_v3"
	case "voice":
		return ds.GetVoice(ctx, key)
	default:
		return nil, errors.MediaTypeUnsupported(_type)
	}

	query := fmt.Sprintf(`
	SELECT
		f.md5,
		f.file_name,
		f.file_size,
		f.modify_time,
		IFNULL(d1.username,""),
		IFNULL(d2.username,"")
	FROM
		%s f
	LEFT JOIN
		dir2id d1 ON d1.rowid = f.dir1
	LEFT JOIN
		dir2id d2 ON d2.rowid = f.dir2
	`, table)
	query += " WHERE f.md5 = ? OR f.file_name LIKE ? || '%'"
	args := []interface{}{key, key}

	// 执行查询
	db, err := ds.dbm.GetDB(Media)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	var media *model.Media
	for rows.Next() {
		var mediaV4 model.MediaV4
		err := rows.Scan(
			&mediaV4.Key,
			&mediaV4.Name,
			&mediaV4.Size,
			&mediaV4.ModifyTime,
			&mediaV4.Dir1,
			&mediaV4.Dir2,
		)
		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}
		mediaV4.Type = _type
		media = mediaV4.Wrap()

		// 跳过缩略图
		if _type == "image" && !strings.Contains(media.Name, "_t") {
			break
		}
	}

	if media == nil {
		return nil, errors.ErrMediaNotFound
	}

	return media, nil
}

func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
	if key == "" {
		return nil, errors.ErrKeyEmpty
	}

	query := `
	SELECT voice_data
	FROM VoiceInfo
	WHERE svr_id = ?
	`
	args := []interface{}{key}

	dbs, err := ds.dbm.GetDBs(Voice)
	if err != nil {
		return nil, errors.DBConnectFailed("", err)
	}

	for _, db := range dbs {
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		for rows.Next() {
			var voiceData []byte
			err := rows.Scan(
				&voiceData,
			)
			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}
			if len(voiceData) > 0 {
				return &model.Media{
					Type: "voice",
					Key:  key,
					Data: voiceData,
				}, nil
			}
		}
	}

	return nil, errors.ErrMediaNotFound
}

func (ds *DataSource) Close() error {
	return ds.dbm.Close()
}

package windowsv3

import (
	"context"
	"encoding/hex"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/fsnotify/fsnotify"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource/dbm"
	"github.com/sjzar/chatlog/pkg/util"
)

const (
	Message = "message"
	Contact = "contact"
	Image   = "image"
	Video   = "video"
	File    = "file"
	Voice   = "voice"
)

var Groups = []*dbm.Group{
	{
		Name:      Message,
		Pattern:   `^MSG([0-9]?[0-9])?\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Contact,
		Pattern:   `^MicroMsg\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Image,
		Pattern:   `^HardLinkImage\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Video,
		Pattern:   `^HardLinkVideo\.db$`,
		BlackList: []string{},
	},
	{
		Name:      File,
		Pattern:   `^HardLinkFile\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Voice,
		Pattern:   `^MediaMSG([0-9])?\.db$`,
		BlackList: []string{},
	},
}

// MessageDBInfo 保存消息数据库的信息
type MessageDBInfo struct {
	FilePath  string
	StartTime time.Time
	EndTime   time.Time
	TalkerMap map[string]int
}

// DataSource 实现了 DataSource 接口
type DataSource struct {
	path string
	dbm  *dbm.DBManager

	// 消息数据库信息
	messageInfos []MessageDBInfo
}

// New 创建一个新的 WindowsV3DataSource
func New(path string) (*DataSource, error) {
	ds := &DataSource{
		path:         path,
		dbm:          dbm.NewDBManager(path),
		messageInfos: make([]MessageDBInfo, 0),
	}

	for _, g := range Groups {
		ds.dbm.AddGroup(g)
	}

	if err := ds.dbm.Start(); err != nil {
		return nil, err
	}

	if err := ds.initMessageDbs(); err != nil {
		return nil, errors.DBInitFailed(err)
	}

	ds.dbm.AddCallback(Message, func(event fsnotify.Event) error {
		if !event.Op.Has(fsnotify.Create) {
			return nil
		}
		if err := ds.initMessageDbs(); err != nil {
			log.Err(err).Msgf("Failed to reinitialize message DBs: %s", event.Name)
		}
		return nil
	})

	return ds, nil
}

func (ds *DataSource) SetCallback(name string, callback func(event fsnotify.Event) error) error {
	if name == "chatroom" {
		name = Contact
	}
	return ds.dbm.AddCallback(name, callback)
}

// initMessageDbs 初始化消息数据库
func (ds *DataSource) initMessageDbs() error {
	// 获取所有消息数据库文件路径
	dbPaths, err := ds.dbm.GetDBPath(Message)
	if err != nil {
		if strings.Contains(err.Error(), "db file not found") {
			ds.messageInfos = make([]MessageDBInfo, 0)
			return nil
		}
		return err
	}

	// 处理每个数据库文件
	infos := make([]MessageDBInfo, 0)
	for _, filePath := range dbPaths {
		db, err := ds.dbm.OpenDB(filePath)
		if err != nil {
			log.Err(err).Msgf("获取数据库 %s 失败", filePath)
			continue
		}

		// 获取 DBInfo 表中的开始时间
		var startTime time.Time

		rows, err := db.Query("SELECT tableIndex, tableVersion, tableDesc FROM DBInfo")
		if err != nil {
			log.Err(err).Msgf("查询数据库 %s 的 DBInfo 表失败", filePath)
			continue
		}

		for rows.Next() {
			var tableIndex int
			var tableVersion int64
			var tableDesc string

			if err := rows.Scan(&tableIndex, &tableVersion, &tableDesc); err != nil {
				log.Err(err).Msg("扫描 DBInfo 行失败")
				continue
			}

			// 查找描述为 "Start Time" 的记录
			if strings.Contains(tableDesc, "Start Time") {
				startTime = time.Unix(tableVersion/1000, (tableVersion%1000)*1000000)
				break
			}
		}
		rows.Close()

		// 组织 TalkerMap
		talkerMap := make(map[string]int)
		rows, err = db.Query("SELECT UsrName FROM Name2ID")
		if err != nil {
			log.Err(err).Msgf("查询数据库 %s 的 Name2ID 表失败", filePath)
			continue
		}

		i := 1
		for rows.Next() {
			var userName string
			if err := rows.Scan(&userName); err != nil {
				log.Err(err).Msg("扫描 Name2ID 行失败")
				continue
			}
			talkerMap[userName] = i
			i++
		}
		rows.Close()

		// 保存数据库信息
		infos = append(infos, MessageDBInfo{
			FilePath:  filePath,
			StartTime: startTime,
			TalkerMap: talkerMap,
		})
	}

	// 按照 StartTime 排序数据库文件
	sort.Slice(infos, func(i, j int) bool {
		return infos[i].StartTime.Before(infos[j].StartTime)
	})

	// 设置结束时间
	for i := range infos {
		if i == len(infos)-1 {
			infos[i].EndTime = time.Now()
		} else {
			infos[i].EndTime = infos[i+1].StartTime
		}
	}
	ds.messageInfos = infos
	return nil
}

// getDBInfosForTimeRange 获取时间范围内的数据库信息
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
	var dbs []MessageDBInfo
	for _, info := range ds.messageInfos {
		if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
			dbs = append(dbs, info)
		}
	}
	return dbs
}

func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
	if talker == "" {
		return nil, errors.ErrTalkerEmpty
	}

	// 解析talker参数，支持多个talker（以英文逗号分隔）
	talkers := util.Str2List(talker, ",")
	if len(talkers) == 0 {
		return nil, errors.ErrTalkerEmpty
	}

	// 找到时间范围内的数据库文件
	dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
	if len(dbInfos) == 0 {
		return nil, errors.TimeRangeNotFound(startTime, endTime)
	}

	// 解析sender参数，支持多个发送者（以英文逗号分隔）
	senders := util.Str2List(sender, ",")

	// 预编译正则表达式（如果有keyword）
	var regex *regexp.Regexp
	if keyword != "" {
		var err error
		regex, err = regexp.Compile(keyword)
		if err != nil {
			return nil, errors.QueryFailed("invalid regex pattern", err)
		}
	}

	// 从每个相关数据库中查询消息
	filteredMessages := []*model.Message{}

	for _, dbInfo := range dbInfos {
		// 检查上下文是否已取消
		if err := ctx.Err(); err != nil {
			return nil, err
		}

		db, err := ds.dbm.OpenDB(dbInfo.FilePath)
		if err != nil {
			log.Error().Msgf("数据库 %s 未打开", dbInfo.FilePath)
			continue
		}

		// 对每个talker进行查询
		for _, talkerItem := range talkers {
			// 构建查询条件
			conditions := []string{"Sequence >= ? AND Sequence <= ?"}
			args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}

			// 添加talker条件
			talkerID, ok := dbInfo.TalkerMap[talkerItem]
			if ok {
				conditions = append(conditions, "TalkerId = ?")
				args = append(args, talkerID)
			} else {
				conditions = append(conditions, "StrTalker = ?")
				args = append(args, talkerItem)
			}

			query := fmt.Sprintf(`
				SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
					Type, SubType, StrContent, CompressContent, BytesExtra
				FROM MSG
				WHERE %s
				ORDER BY Sequence ASC
			`, strings.Join(conditions, " AND "))

			// 执行查询
			rows, err := db.QueryContext(ctx, query, args...)
			if err != nil {
				// 如果表不存在，跳过此talker
				if strings.Contains(err.Error(), "no such table") {
					continue
				}
				log.Err(err).Msgf("从数据库 %s 查询消息失败", dbInfo.FilePath)
				continue
			}

			// 处理查询结果，在读取时进行过滤
			for rows.Next() {
				var msg model.MessageV3
				var compressContent []byte
				var bytesExtra []byte

				err := rows.Scan(
					&msg.MsgSvrID,
					&msg.Sequence,
					&msg.CreateTime,
					&msg.StrTalker,
					&msg.IsSender,
					&msg.Type,
					&msg.SubType,
					&msg.StrContent,
					&compressContent,
					&bytesExtra,
				)
				if err != nil {
					rows.Close()
					return nil, errors.ScanRowFailed(err)
				}
				msg.CompressContent = compressContent
				msg.BytesExtra = bytesExtra

				// 将消息转换为标准格式
				message := msg.Wrap()

				// 应用sender过滤
				if len(senders) > 0 {
					senderMatch := false
					for _, s := range senders {
						if message.Sender == s {
							senderMatch = true
							break
						}
					}
					if !senderMatch {
						continue // 不匹配sender，跳过此消息
					}
				}

				// 应用keyword过滤
				if regex != nil {
					plainText := message.PlainTextContent()
					if !regex.MatchString(plainText) {
						continue // 不匹配keyword，跳过此消息
					}
				}

				// 通过所有过滤条件，保留此消息
				filteredMessages = append(filteredMessages, message)

				// 检查是否已经满足分页处理数量
				if limit > 0 && len(filteredMessages) >= offset+limit {
					// 已经获取了足够的消息，可以提前返回
					rows.Close()

					// 对所有消息按时间排序
					sort.Slice(filteredMessages, func(i, j int) bool {
						return filteredMessages[i].Seq < filteredMessages[j].Seq
					})

					// 处理分页
					if offset >= len(filteredMessages) {
						return []*model.Message{}, nil
					}
					end := offset + limit
					if end > len(filteredMessages) {
						end = len(filteredMessages)
					}
					return filteredMessages[offset:end], nil
				}
			}
			rows.Close()
		}
	}

	// 对所有消息按时间排序
	sort.Slice(filteredMessages, func(i, j int) bool {
		return filteredMessages[i].Seq < filteredMessages[j].Seq
	})

	// 处理分页
	if limit > 0 {
		if offset >= len(filteredMessages) {
			return []*model.Message{}, nil
		}
		end := offset + limit
		if end > len(filteredMessages) {
			end = len(filteredMessages)
		}
		return filteredMessages[offset:end], nil
	}

	return filteredMessages, nil
}

// GetContacts 实现获取联系人信息的方法
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact
                WHERE UserName = ? OR Alias = ? OR Remark = ? OR NickName = ?`
		args = []interface{}{key, key, key, key}
	} else {
		// 查询所有联系人
		query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact`
	}

	// 添加排序、分页
	query += ` ORDER BY UserName`
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Contact)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	contacts := []*model.Contact{}
	for rows.Next() {
		var contactV3 model.ContactV3
		err := rows.Scan(
			&contactV3.UserName,
			&contactV3.Alias,
			&contactV3.Remark,
			&contactV3.NickName,
			&contactV3.Reserved1,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		contacts = append(contacts, contactV3.Wrap())
	}

	return contacts, nil
}

// GetChatRooms 实现获取群聊信息的方法
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`
		args = []interface{}{key}

		// 执行查询
		db, err := ds.dbm.GetDB(Contact)
		if err != nil {
			return nil, err
		}
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		chatRooms := []*model.ChatRoom{}
		for rows.Next() {
			var chatRoomV3 model.ChatRoomV3
			err := rows.Scan(
				&chatRoomV3.ChatRoomName,
				&chatRoomV3.Reserved2,
				&chatRoomV3.RoomData,
			)

			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}

			chatRooms = append(chatRooms, chatRoomV3.Wrap())
		}

		// 如果没有找到群聊，尝试通过联系人查找
		if len(chatRooms) == 0 {
			contacts, err := ds.GetContacts(ctx, key, 1, 0)
			if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
				// 再次尝试通过用户名查找群聊
				rows, err := db.QueryContext(ctx,
					`SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`,
					contacts[0].UserName)

				if err != nil {
					return nil, errors.QueryFailed(query, err)
				}
				defer rows.Close()

				for rows.Next() {
					var chatRoomV3 model.ChatRoomV3
					err := rows.Scan(
						&chatRoomV3.ChatRoomName,
						&chatRoomV3.Reserved2,
						&chatRoomV3.RoomData,
					)

					if err != nil {
						return nil, errors.ScanRowFailed(err)
					}

					chatRooms = append(chatRooms, chatRoomV3.Wrap())
				}

				// 如果群聊记录不存在，但联系人记录存在，创建一个模拟的群聊对象
				if len(chatRooms) == 0 {
					chatRooms = append(chatRooms, &model.ChatRoom{
						Name:             contacts[0].UserName,
						Users:            make([]model.ChatRoomUser, 0),
						User2DisplayName: make(map[string]string),
					})
				}
			}
		}

		return chatRooms, nil
	} else {
		// 查询所有群聊
		query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom`

		// 添加排序、分页
		query += ` ORDER BY ChatRoomName`
		if limit > 0 {
			query += fmt.Sprintf(" LIMIT %d", limit)
			if offset > 0 {
				query += fmt.Sprintf(" OFFSET %d", offset)
			}
		}

		// 执行查询
		db, err := ds.dbm.GetDB(Contact)
		if err != nil {
			return nil, err
		}
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		chatRooms := []*model.ChatRoom{}
		for rows.Next() {
			var chatRoomV3 model.ChatRoomV3
			err := rows.Scan(
				&chatRoomV3.ChatRoomName,
				&chatRoomV3.Reserved2,
				&chatRoomV3.RoomData,
			)

			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}

			chatRooms = append(chatRooms, chatRoomV3.Wrap())
		}

		return chatRooms, nil
	}
}

// GetSessions 实现获取会话信息的方法
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
                FROM Session
                WHERE strUsrName = ? OR strNickName = ?
                ORDER BY nOrder DESC`
		args = []interface{}{key, key}
	} else {
		// 查询所有会话
		query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
                FROM Session
                ORDER BY nOrder DESC`
	}

	// 添加分页
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Contact)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	sessions := []*model.Session{}
	for rows.Next() {
		var sessionV3 model.SessionV3
		err := rows.Scan(
			&sessionV3.StrUsrName,
			&sessionV3.NOrder,
			&sessionV3.StrNickName,
			&sessionV3.StrContent,
			&sessionV3.NTime,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		sessions = append(sessions, sessionV3.Wrap())
	}

	return sessions, nil
}

func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
	if key == "" {
		return nil, errors.ErrKeyEmpty
	}

	if _type == "voice" {
		return ds.GetVoice(ctx, key)
	}

	md5key, err := hex.DecodeString(key)
	if err != nil {
		return nil, errors.DecodeKeyFailed(err)
	}

	var dbType string
	var table1, table2 string

	switch _type {
	case "image":
		dbType = Image
		table1 = "HardLinkImageAttribute"
		table2 = "HardLinkImageID"
	case "video":
		dbType = Video
		table1 = "HardLinkVideoAttribute"
		table2 = "HardLinkVideoID"
	case "file":
		dbType = File
		table1 = "HardLinkFileAttribute"
		table2 = "HardLinkFileID"
	default:
		return nil, errors.MediaTypeUnsupported(_type)
	}

	db, err := ds.dbm.GetDB(dbType)
	if err != nil {
		return nil, err
	}

	query := fmt.Sprintf(`
        SELECT
            a.FileName,
            a.ModifyTime,
            IFNULL(d1.Dir,"") AS Dir1,
            IFNULL(d2.Dir,"") AS Dir2
        FROM
            %s a
        LEFT JOIN
            %s d1 ON a.DirID1 = d1.DirId
        LEFT JOIN
            %s d2 ON a.DirID2 = d2.DirId
        WHERE
            a.Md5 = ?
    `, table1, table2, table2)
	args := []interface{}{md5key}

	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	var media *model.Media
	for rows.Next() {
		var mediaV3 model.MediaV3
		err := rows.Scan(
			&mediaV3.Name,
			&mediaV3.ModifyTime,
			&mediaV3.Dir1,
			&mediaV3.Dir2,
		)
		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}
		mediaV3.Type = _type
		mediaV3.Key = key
		media = mediaV3.Wrap()
	}

	if media == nil {
		return nil, errors.ErrMediaNotFound
	}

	return media, nil
}

func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
	if key == "" {
		return nil, errors.ErrKeyEmpty
	}

	query := `
	SELECT Buf
	FROM Media
	WHERE Reserved0 = ?
	`
	args := []interface{}{key}

	dbs, err := ds.dbm.GetDBs(Voice)
	if err != nil {
		return nil, errors.DBConnectFailed("", err)
	}

	for _, db := range dbs {
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, errors.QueryFailed(query, err)
		}
		defer rows.Close()

		for rows.Next() {
			var voiceData []byte
			err := rows.Scan(
				&voiceData,
			)
			if err != nil {
				return nil, errors.ScanRowFailed(err)
			}
			if len(voiceData) > 0 {
				return &model.Media{
					Type: "voice",
					Key:  key,
					Data: voiceData,
				}, nil
			}
		}
	}

	return nil, errors.ErrMediaNotFound
}

// Close 实现 DataSource 接口的 Close 方法
func (ds *DataSource) Close() error {
	return ds.dbm.Close()
}

package chatlog

import (
	"fmt"
	"path/filepath"
	"runtime"
	"time"

	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/ui/footer"
	"github.com/sjzar/chatlog/internal/ui/form"
	"github.com/sjzar/chatlog/internal/ui/help"
	"github.com/sjzar/chatlog/internal/ui/infobar"
	"github.com/sjzar/chatlog/internal/ui/menu"
	"github.com/sjzar/chatlog/internal/wechat"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

const (
	RefreshInterval = 1000 * time.Millisecond
)

type App struct {
	*tview.Application

	ctx         *ctx.Context
	m           *Manager
	stopRefresh chan struct{}

	// page
	mainPages *tview.Pages
	infoBar   *infobar.InfoBar
	tabPages  *tview.Pages
	footer    *footer.Footer

	// tab
	menu      *menu.Menu
	help      *help.Help
	activeTab int
	tabCount  int
}

func NewApp(ctx *ctx.Context, m *Manager) *App {
	app := &App{
		ctx:         ctx,
		m:           m,
		Application: tview.NewApplication(),
		mainPages:   tview.NewPages(),
		infoBar:     infobar.New(),
		tabPages:    tview.NewPages(),
		footer:      footer.New(),
		menu:        menu.New("主菜单"),
		help:        help.New(),
	}

	app.initMenu()

	app.updateMenuItemsState()

	return app
}

func (a *App) Run() error {

	flex := tview.NewFlex().
		SetDirection(tview.FlexRow).
		AddItem(a.infoBar, infobar.InfoBarViewHeight, 0, false).
		AddItem(a.tabPages, 0, 1, true).
		AddItem(a.footer, 1, 1, false)

	a.mainPages.AddPage("main", flex, true, true)

	a.tabPages.
		AddPage("0", a.menu, true, true).
		AddPage("1", a.help, true, false)
	a.tabCount = 2

	a.SetInputCapture(a.inputCapture)

	go a.refresh()

	if err := a.SetRoot(a.mainPages, true).EnableMouse(false).Run(); err != nil {
		return err
	}

	return nil
}

func (a *App) Stop() {
	// 添加一个通道用于停止刷新 goroutine
	if a.stopRefresh != nil {
		close(a.stopRefresh)
	}
	a.Application.Stop()
}

func (a *App) updateMenuItemsState() {
	// 查找并更新自动解密菜单项
	for _, item := range a.menu.GetItems() {
		// 更新自动解密菜单项
		if item.Index == 5 {
			if a.ctx.AutoDecrypt {
				item.Name = "停止自动解密"
				item.Description = "停止监控数据目录更新，不再自动解密新增数据"
			} else {
				item.Name = "开启自动解密"
				item.Description = "监控数据目录更新，自动解密新增数据"
			}
		}

		// 更新HTTP服务菜单项
		if item.Index == 4 {
			if a.ctx.HTTPEnabled {
				item.Name = "停止 HTTP 服务"
				item.Description = "停止本地 HTTP & MCP 服务器"
			} else {
				item.Name = "启动 HTTP 服务"
				item.Description = "启动本地 HTTP & MCP 服务器"
			}
		}
	}
}

func (a *App) switchTab(step int) {
	index := (a.activeTab + step) % a.tabCount
	if index < 0 {
		index = a.tabCount - 1
	}
	a.activeTab = index
	a.tabPages.SwitchToPage(fmt.Sprint(a.activeTab))
}

func (a *App) refresh() {
	tick := time.NewTicker(RefreshInterval)
	defer tick.Stop()

	for {
		select {
		case <-a.stopRefresh:
			return
		case <-tick.C:
			if a.ctx.AutoDecrypt || a.ctx.HTTPEnabled {
				a.m.RefreshSession()
			}
			a.infoBar.UpdateAccount(a.ctx.Account)
			a.infoBar.UpdateBasicInfo(a.ctx.PID, a.ctx.FullVersion, a.ctx.ExePath)
			a.infoBar.UpdateStatus(a.ctx.Status)
			a.infoBar.UpdateDataKey(a.ctx.DataKey)
			a.infoBar.UpdatePlatform(a.ctx.Platform)
			a.infoBar.UpdateDataUsageDir(a.ctx.DataUsage, a.ctx.DataDir)
			a.infoBar.UpdateWorkUsageDir(a.ctx.WorkUsage, a.ctx.WorkDir)
			if a.ctx.LastSession.Unix() > 1000000000 {
				a.infoBar.UpdateSession(a.ctx.LastSession.Format("2006-01-02 15:04:05"))
			}
			if a.ctx.HTTPEnabled {
				a.infoBar.UpdateHTTPServer(fmt.Sprintf("[green][已启动][white] [%s]", a.ctx.HTTPAddr))
			} else {
				a.infoBar.UpdateHTTPServer("[未启动]")
			}
			if a.ctx.AutoDecrypt {
				a.infoBar.UpdateAutoDecrypt("[green][已开启][white]")
			} else {
				a.infoBar.UpdateAutoDecrypt("[未开启]")
			}

			a.Draw()
		}
	}
}

func (a *App) inputCapture(event *tcell.EventKey) *tcell.EventKey {

	// 如果当前页面不是主页面，ESC 键返回主页面
	if a.mainPages.HasPage("submenu") && event.Key() == tcell.KeyEscape {
		a.mainPages.RemovePage("submenu")
		a.mainPages.SwitchToPage("main")
		return nil
	}

	if a.tabPages.HasFocus() {
		switch event.Key() {
		case tcell.KeyLeft:
			a.switchTab(-1)
			return nil
		case tcell.KeyRight:
			a.switchTab(1)
			return nil
		}
	}

	switch event.Key() {
	case tcell.KeyCtrlC:
		a.Stop()
	}

	return event
}

func (a *App) initMenu() {
	getDataKey := &menu.Item{
		Index:       2,
		Name:        "获取数据密钥",
		Description: "从进程获取数据密钥",
		Selected: func(i *menu.Item) {
			modal := tview.NewModal()
			if runtime.GOOS == "darwin" {
				modal.SetText("获取数据密钥中...\n预计需要 20 秒左右的时间，期间微信会卡住，请耐心等待")
			} else {
				modal.SetText("获取数据密钥中...")
			}
			a.mainPages.AddPage("modal", modal, true, true)
			a.SetFocus(modal)

			go func() {
				err := a.m.GetDataKey()

				// 在主线程中更新UI
				a.QueueUpdateDraw(func() {
					if err != nil {
						// 解密失败
						modal.SetText("获取数据密钥失败: " + err.Error())
					} else {
						// 解密成功
						modal.SetText("获取数据密钥成功")
					}

					// 添加确认按钮
					modal.AddButtons([]string{"OK"})
					modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
						a.mainPages.RemovePage("modal")
					})
					a.SetFocus(modal)
				})
			}()
		},
	}

	decryptData := &menu.Item{
		Index:       3,
		Name:        "解密数据",
		Description: "解密数据文件",
		Selected: func(i *menu.Item) {
			// 创建一个没有按钮的模态框，显示"解密中..."
			modal := tview.NewModal().
				SetText("解密中...")

			a.mainPages.AddPage("modal", modal, true, true)
			a.SetFocus(modal)

			// 在后台执行解密操作
			go func() {
				// 执行解密
				err := a.m.DecryptDBFiles()

				// 在主线程中更新UI
				a.QueueUpdateDraw(func() {
					if err != nil {
						// 解密失败
						modal.SetText("解密失败: " + err.Error())
					} else {
						// 解密成功
						modal.SetText("解密数据成功")
					}

					// 添加确认按钮
					modal.AddButtons([]string{"OK"})
					modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
						a.mainPages.RemovePage("modal")
					})
					a.SetFocus(modal)
				})
			}()
		},
	}

	httpServer := &menu.Item{
		Index:       4,
		Name:        "启动 HTTP 服务",
		Description: "启动本地 HTTP & MCP 服务器",
		Selected: func(i *menu.Item) {
			modal := tview.NewModal()

			// 根据当前服务状态执行不同操作
			if !a.ctx.HTTPEnabled {
				// HTTP 服务未启动，启动服务
				modal.SetText("正在启动 HTTP 服务...")
				a.mainPages.AddPage("modal", modal, true, true)
				a.SetFocus(modal)

				// 在后台启动服务
				go func() {
					err := a.m.StartService()

					// 在主线程中更新UI
					a.QueueUpdateDraw(func() {
						if err != nil {
							// 启动失败
							modal.SetText("启动 HTTP 服务失败: " + err.Error())
						} else {
							// 启动成功
							modal.SetText("已启动 HTTP 服务")
						}

						// 更改菜单项名称
						a.updateMenuItemsState()

						// 添加确认按钮
						modal.AddButtons([]string{"OK"})
						modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
							a.mainPages.RemovePage("modal")
						})
						a.SetFocus(modal)
					})
				}()
			} else {
				// HTTP 服务已启动，停止服务
				modal.SetText("正在停止 HTTP 服务...")
				a.mainPages.AddPage("modal", modal, true, true)
				a.SetFocus(modal)

				// 在后台停止服务
				go func() {
					err := a.m.StopService()

					// 在主线程中更新UI
					a.QueueUpdateDraw(func() {
						if err != nil {
							// 停止失败
							modal.SetText("停止 HTTP 服务失败: " + err.Error())
						} else {
							// 停止成功
							modal.SetText("已停止 HTTP 服务")
						}

						// 更改菜单项名称
						a.updateMenuItemsState()

						// 添加确认按钮
						modal.AddButtons([]string{"OK"})
						modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
							a.mainPages.RemovePage("modal")
						})
						a.SetFocus(modal)
					})
				}()
			}
		},
	}

	autoDecrypt := &menu.Item{
		Index:       5,
		Name:        "开启自动解密",
		Description: "自动解密新增的数据文件",
		Selected: func(i *menu.Item) {
			modal := tview.NewModal()

			// 根据当前自动解密状态执行不同操作
			if !a.ctx.AutoDecrypt {
				// 自动解密未开启，开启自动解密
				modal.SetText("正在开启自动解密...")
				a.mainPages.AddPage("modal", modal, true, true)
				a.SetFocus(modal)

				// 在后台开启自动解密
				go func() {
					err := a.m.StartAutoDecrypt()

					// 在主线程中更新UI
					a.QueueUpdateDraw(func() {
						if err != nil {
							// 开启失败
							modal.SetText("开启自动解密失败: " + err.Error())
						} else {
							// 开启成功
							if a.ctx.Version == 3 {
								modal.SetText("已开启自动解密\n3.x版本数据文件更新不及时，有低延迟需求请使用4.0版本")
							} else {
								modal.SetText("已开启自动解密")
							}
						}

						// 更改菜单项名称
						a.updateMenuItemsState()

						// 添加确认按钮
						modal.AddButtons([]string{"OK"})
						modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
							a.mainPages.RemovePage("modal")
						})
						a.SetFocus(modal)
					})
				}()
			} else {
				// 自动解密已开启，停止自动解密
				modal.SetText("正在停止自动解密...")
				a.mainPages.AddPage("modal", modal, true, true)
				a.SetFocus(modal)

				// 在后台停止自动解密
				go func() {
					err := a.m.StopAutoDecrypt()

					// 在主线程中更新UI
					a.QueueUpdateDraw(func() {
						if err != nil {
							// 停止失败
							modal.SetText("停止自动解密失败: " + err.Error())
						} else {
							// 停止成功
							modal.SetText("已停止自动解密")
						}

						// 更改菜单项名称
						a.updateMenuItemsState()

						// 添加确认按钮
						modal.AddButtons([]string{"OK"})
						modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
							a.mainPages.RemovePage("modal")
						})
						a.SetFocus(modal)
					})
				}()
			}
		},
	}

	setting := &menu.Item{
		Index:       6,
		Name:        "设置",
		Description: "设置应用程序选项",
		Selected:    a.settingSelected,
	}

	selectAccount := &menu.Item{
		Index:       7,
		Name:        "切换账号",
		Description: "切换当前操作的账号，可以选择进程或历史账号",
		Selected:    a.selectAccountSelected,
	}

	a.menu.AddItem(getDataKey)
	a.menu.AddItem(decryptData)
	a.menu.AddItem(httpServer)
	a.menu.AddItem(autoDecrypt)
	a.menu.AddItem(setting)
	a.menu.AddItem(selectAccount)

	a.menu.AddItem(&menu.Item{
		Index:       8,
		Name:        "退出",
		Description: "退出程序",
		Selected: func(i *menu.Item) {
			a.Stop()
		},
	})
}

// settingItem 表示一个设置项
type settingItem struct {
	name        string
	description string
	action      func()
}

func (a *App) settingSelected(i *menu.Item) {

	settings := []settingItem{
		{
			name:        "设置 HTTP 服务地址",
			description: "配置 HTTP 服务监听的地址",
			action:      a.settingHTTPPort,
		},
		{
			name:        "设置工作目录",
			description: "配置数据解密后的存储目录",
			action:      a.settingWorkDir,
		},
		{
			name:        "设置数据密钥",
			description: "配置数据解密密钥",
			action:      a.settingDataKey,
		},
		{
			name:        "设置数据目录",
			description: "配置微信数据文件所在目录",
			action:      a.settingDataDir,
		},
	}

	subMenu := menu.NewSubMenu("设置")
	for idx, setting := range settings {
		item := &menu.Item{
			Index:       idx + 1,
			Name:        setting.name,
			Description: setting.description,
			Selected: func(action func()) func(*menu.Item) {
				return func(*menu.Item) {
					action()
				}
			}(setting.action),
		}
		subMenu.AddItem(item)
	}

	a.mainPages.AddPage("submenu", subMenu, true, true)
	a.SetFocus(subMenu)
}

// settingHTTPPort 设置 HTTP 端口
func (a *App) settingHTTPPort() {
	// 使用我们的自定义表单组件
	formView := form.NewForm("设置 HTTP 地址")

	// 临时存储用户输入的值
	tempHTTPAddr := a.ctx.HTTPAddr

	// 添加输入字段 - 不再直接设置HTTP地址，而是更新临时变量
	formView.AddInputField("地址", tempHTTPAddr, 0, nil, func(text string) {
		tempHTTPAddr = text // 只更新临时变量
	})

	// 添加按钮 - 点击保存时才设置HTTP地址
	formView.AddButton("保存", func() {
		a.m.SetHTTPAddr(tempHTTPAddr) // 在这里设置HTTP地址
		a.mainPages.RemovePage("submenu2")
		a.showInfo("HTTP 地址已设置为 " + a.ctx.HTTPAddr)
	})

	formView.AddButton("取消", func() {
		a.mainPages.RemovePage("submenu2")
	})

	a.mainPages.AddPage("submenu2", formView, true, true)
	a.SetFocus(formView)
}

// settingWorkDir 设置工作目录
func (a *App) settingWorkDir() {
	// 使用我们的自定义表单组件
	formView := form.NewForm("设置工作目录")

	// 临时存储用户输入的值
	tempWorkDir := a.ctx.WorkDir

	// 添加输入字段 - 不再直接设置工作目录，而是更新临时变量
	formView.AddInputField("工作目录", tempWorkDir, 0, nil, func(text string) {
		tempWorkDir = text // 只更新临时变量
	})

	// 添加按钮 - 点击保存时才设置工作目录
	formView.AddButton("保存", func() {
		a.ctx.SetWorkDir(tempWorkDir) // 在这里设置工作目录
		a.mainPages.RemovePage("submenu2")
		a.showInfo("工作目录已设置为 " + a.ctx.WorkDir)
	})

	formView.AddButton("取消", func() {
		a.mainPages.RemovePage("submenu2")
	})

	a.mainPages.AddPage("submenu2", formView, true, true)
	a.SetFocus(formView)
}

// settingDataKey 设置数据密钥
func (a *App) settingDataKey() {
	// 使用我们的自定义表单组件
	formView := form.NewForm("设置数据密钥")

	// 临时存储用户输入的值
	tempDataKey := a.ctx.DataKey

	// 添加输入字段 - 不直接设置数据密钥，而是更新临时变量
	formView.AddInputField("数据密钥", tempDataKey, 0, nil, func(text string) {
		tempDataKey = text // 只更新临时变量
	})

	// 添加按钮 - 点击保存时才设置数据密钥
	formView.AddButton("保存", func() {
		a.ctx.DataKey = tempDataKey // 设置数据密钥
		a.mainPages.RemovePage("submenu2")
		a.showInfo("数据密钥已设置")
	})

	formView.AddButton("取消", func() {
		a.mainPages.RemovePage("submenu2")
	})

	a.mainPages.AddPage("submenu2", formView, true, true)
	a.SetFocus(formView)
}

// settingDataDir 设置数据目录
func (a *App) settingDataDir() {
	// 使用我们的自定义表单组件
	formView := form.NewForm("设置数据目录")

	// 临时存储用户输入的值
	tempDataDir := a.ctx.DataDir

	// 添加输入字段 - 不直接设置数据目录，而是更新临时变量
	formView.AddInputField("数据目录", tempDataDir, 0, nil, func(text string) {
		tempDataDir = text // 只更新临时变量
	})

	// 添加按钮 - 点击保存时才设置数据目录
	formView.AddButton("保存", func() {
		a.ctx.DataDir = tempDataDir // 设置数据目录
		a.mainPages.RemovePage("submenu2")
		a.showInfo("数据目录已设置为 " + a.ctx.DataDir)
	})

	formView.AddButton("取消", func() {
		a.mainPages.RemovePage("submenu2")
	})

	a.mainPages.AddPage("submenu2", formView, true, true)
	a.SetFocus(formView)
}

// selectAccountSelected 处理切换账号菜单项的选择事件
func (a *App) selectAccountSelected(i *menu.Item) {
	// 创建子菜单
	subMenu := menu.NewSubMenu("切换账号")

	// 添加微信进程
	instances := a.m.wechat.GetWeChatInstances()
	if len(instances) > 0 {
		// 添加实例标题
		subMenu.AddItem(&menu.Item{
			Index:       0,
			Name:        "--- 微信进程 ---",
			Description: "",
			Hidden:      false,
			Selected:    nil,
		})

		// 添加实例列表
		for idx, instance := range instances {
			// 创建一个实例描述
			description := fmt.Sprintf("版本: %s 目录: %s", instance.FullVersion, instance.DataDir)

			// 标记当前选中的实例
			name := fmt.Sprintf("%s [%d]", instance.Name, instance.PID)
			if a.ctx.Current != nil && a.ctx.Current.PID == instance.PID {
				name = name + " [当前]"
			}

			// 创建菜单项
			instanceItem := &menu.Item{
				Index:       idx + 1,
				Name:        name,
				Description: description,
				Hidden:      false,
				Selected: func(instance *wechat.Account) func(*menu.Item) {
					return func(*menu.Item) {
						// 如果是当前账号，则无需切换
						if a.ctx.Current != nil && a.ctx.Current.PID == instance.PID {
							a.mainPages.RemovePage("submenu")
							a.showInfo("已经是当前账号")
							return
						}

						// 显示切换中的模态框
						modal := tview.NewModal().SetText("正在切换账号...")
						a.mainPages.AddPage("modal", modal, true, true)
						a.SetFocus(modal)

						// 在后台执行切换操作
						go func() {
							err := a.m.Switch(instance, "")

							// 在主线程中更新UI
							a.QueueUpdateDraw(func() {
								a.mainPages.RemovePage("modal")
								a.mainPages.RemovePage("submenu")

								if err != nil {
									// 切换失败
									a.showError(fmt.Errorf("切换账号失败: %v", err))
								} else {
									// 切换成功
									a.showInfo("切换账号成功")
									// 更新菜单状态
									a.updateMenuItemsState()
								}
							})
						}()
					}
				}(instance),
			}
			subMenu.AddItem(instanceItem)
		}
	}

	// 添加历史账号
	if len(a.ctx.History) > 0 {
		// 添加历史账号标题
		subMenu.AddItem(&menu.Item{
			Index:       100,
			Name:        "--- 历史账号 ---",
			Description: "",
			Hidden:      false,
			Selected:    nil,
		})

		// 添加历史账号列表
		idx := 101
		for account, hist := range a.ctx.History {
			// 创建一个账号描述
			description := fmt.Sprintf("版本: %s 目录: %s", hist.FullVersion, hist.DataDir)

			// 标记当前选中的账号
			name := account
			if name == "" {
				name = filepath.Base(hist.DataDir)
			}
			if a.ctx.DataDir == hist.DataDir {
				name = name + " [当前]"
			}

			// 创建菜单项
			histItem := &menu.Item{
				Index:       idx,
				Name:        name,
				Description: description,
				Hidden:      false,
				Selected: func(account string) func(*menu.Item) {
					return func(*menu.Item) {
						// 如果是当前账号，则无需切换
						if a.ctx.Current != nil && a.ctx.DataDir == a.ctx.History[account].DataDir {
							a.mainPages.RemovePage("submenu")
							a.showInfo("已经是当前账号")
							return
						}

						// 显示切换中的模态框
						modal := tview.NewModal().SetText("正在切换账号...")
						a.mainPages.AddPage("modal", modal, true, true)
						a.SetFocus(modal)

						// 在后台执行切换操作
						go func() {
							err := a.m.Switch(nil, account)

							// 在主线程中更新UI
							a.QueueUpdateDraw(func() {
								a.mainPages.RemovePage("modal")
								a.mainPages.RemovePage("submenu")

								if err != nil {
									// 切换失败
									a.showError(fmt.Errorf("切换账号失败: %v", err))
								} else {
									// 切换成功
									a.showInfo("切换账号成功")
									// 更新菜单状态
									a.updateMenuItemsState()
								}
							})
						}()
					}
				}(account),
			}
			idx++
			subMenu.AddItem(histItem)
		}
	}

	// 如果没有账号可选择
	if len(a.ctx.History) == 0 && len(instances) == 0 {
		subMenu.AddItem(&menu.Item{
			Index:       1,
			Name:        "无可用账号",
			Description: "未检测到微信进程或历史账号",
			Hidden:      false,
			Selected:    nil,
		})
	}

	// 显示子菜单
	a.mainPages.AddPage("submenu", subMenu, true, true)
	a.SetFocus(subMenu)
}

// showModal 显示一个模态对话框
func (a *App) showModal(text string, buttons []string, doneFunc func(buttonIndex int, buttonLabel string)) {
	modal := tview.NewModal().
		SetText(text).
		AddButtons(buttons).
		SetDoneFunc(doneFunc)

	a.mainPages.AddPage("modal", modal, true, true)
	a.SetFocus(modal)
}

// showError 显示错误对话框
func (a *App) showError(err error) {
	a.showModal(err.Error(), []string{"OK"}, func(buttonIndex int, buttonLabel string) {
		a.mainPages.RemovePage("modal")
	})
}

// showInfo 显示信息对话框
func (a *App) showInfo(text string) {
	a.showModal(text, []string{"OK"}, func(buttonIndex int, buttonLabel string) {
		a.mainPages.RemovePage("modal")
	})
}

package darwinv3

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/fsnotify/fsnotify"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource/dbm"
	"github.com/sjzar/chatlog/pkg/util"
)

const (
	Message  = "message"
	Contact  = "contact"
	ChatRoom = "chatroom"
	Session  = "session"
	Media    = "media"
)

var Groups = []*dbm.Group{
	{
		Name:      Message,
		Pattern:   `^msg_([0-9]?[0-9])?\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Contact,
		Pattern:   `^wccontact_new2\.db$`,
		BlackList: []string{},
	},
	{
		Name:      ChatRoom,
		Pattern:   `group_new\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Session,
		Pattern:   `^session_new\.db$`,
		BlackList: []string{},
	},
	{
		Name:      Media,
		Pattern:   `^hldata\.db$`,
		BlackList: []string{},
	},
}

type DataSource struct {
	path string
	dbm  *dbm.DBManager

	talkerDBMap      map[string]string
	user2DisplayName map[string]string
}

func New(path string) (*DataSource, error) {
	ds := &DataSource{
		path:             path,
		dbm:              dbm.NewDBManager(path),
		talkerDBMap:      make(map[string]string),
		user2DisplayName: make(map[string]string),
	}

	for _, g := range Groups {
		ds.dbm.AddGroup(g)
	}

	if err := ds.dbm.Start(); err != nil {
		return nil, err
	}

	if err := ds.initMessageDbs(); err != nil {
		return nil, errors.DBInitFailed(err)
	}
	if err := ds.initChatRoomDb(); err != nil {
		return nil, errors.DBInitFailed(err)
	}

	ds.dbm.AddCallback(Message, func(event fsnotify.Event) error {
		if !event.Op.Has(fsnotify.Create) {
			return nil
		}
		if err := ds.initMessageDbs(); err != nil {
			log.Err(err).Msgf("Failed to reinitialize message DBs: %s", event.Name)
		}
		return nil
	})
	ds.dbm.AddCallback(ChatRoom, func(event fsnotify.Event) error {
		if !event.Op.Has(fsnotify.Create) {
			return nil
		}
		if err := ds.initChatRoomDb(); err != nil {
			log.Err(err).Msgf("Failed to reinitialize chatroom DB: %s", event.Name)
		}
		return nil
	})

	return ds, nil
}

func (ds *DataSource) SetCallback(name string, callback func(event fsnotify.Event) error) error {
	return ds.dbm.AddCallback(name, callback)
}

func (ds *DataSource) initMessageDbs() error {

	dbPaths, err := ds.dbm.GetDBPath(Message)
	if err != nil {
		if strings.Contains(err.Error(), "db file not found") {
			ds.talkerDBMap = make(map[string]string)
			return nil
		}
		return err
	}
	// 处理每个数据库文件
	talkerDBMap := make(map[string]string)
	for _, filePath := range dbPaths {
		db, err := ds.dbm.OpenDB(filePath)
		if err != nil {
			log.Err(err).Msgf("获取数据库 %s 失败", filePath)
			continue
		}

		// 获取所有表名
		rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
		if err != nil {
			log.Err(err).Msgf("数据库 %s 中没有 Chat 表", filePath)
			continue
		}

		for rows.Next() {
			var tableName string
			if err := rows.Scan(&tableName); err != nil {
				log.Err(err).Msgf("数据库 %s 扫描表名失败", filePath)
				continue
			}

			// 从表名中提取可能的talker信息
			talkerMd5 := extractTalkerFromTableName(tableName)
			if talkerMd5 == "" {
				continue
			}
			talkerDBMap[talkerMd5] = filePath
		}
		rows.Close()
	}
	ds.talkerDBMap = talkerDBMap
	return nil
}

func (ds *DataSource) initChatRoomDb() error {
	db, err := ds.dbm.GetDB(ChatRoom)
	if err != nil {
		if strings.Contains(err.Error(), "db file not found") {
			ds.user2DisplayName = make(map[string]string)
			return nil
		}
		return err
	}

	rows, err := db.Query("SELECT m_nsUsrName, IFNULL(nickname,\"\") FROM GroupMember")
	if err != nil {
		log.Err(err).Msg("获取群聊成员失败")
		return nil
	}

	user2DisplayName := make(map[string]string)
	for rows.Next() {
		var user string
		var nickName string
		if err := rows.Scan(&user, &nickName); err != nil {
			log.Err(err).Msg("扫描表名失败")
			continue
		}
		user2DisplayName[user] = nickName
	}
	rows.Close()
	ds.user2DisplayName = user2DisplayName

	return nil
}

func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
	if talker == "" {
		return nil, errors.ErrTalkerEmpty
	}

	// 解析talker参数，支持多个talker（以英文逗号分隔）
	talkers := util.Str2List(talker, ",")
	if len(talkers) == 0 {
		return nil, errors.ErrTalkerEmpty
	}

	// 解析sender参数，支持多个发送者（以英文逗号分隔）
	senders := util.Str2List(sender, ",")

	// 预编译正则表达式（如果有keyword）
	var regex *regexp.Regexp
	if keyword != "" {
		var err error
		regex, err = regexp.Compile(keyword)
		if err != nil {
			return nil, errors.QueryFailed("invalid regex pattern", err)
		}
	}

	// 从每个相关数据库中查询消息，并在读取时进行过滤
	filteredMessages := []*model.Message{}

	// 对每个talker进行查询
	for _, talkerItem := range talkers {
		// 检查上下文是否已取消
		if err := ctx.Err(); err != nil {
			return nil, err
		}

		// 在 darwinv3 中，需要先找到对应的数据库
		_talkerMd5Bytes := md5.Sum([]byte(talkerItem))
		talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
		dbPath, ok := ds.talkerDBMap[talkerMd5]
		if !ok {
			// 如果找不到对应的数据库，跳过此talker
			continue
		}

		db, err := ds.dbm.OpenDB(dbPath)
		if err != nil {
			log.Error().Msgf("数据库 %s 未打开", dbPath)
			continue
		}

		tableName := fmt.Sprintf("Chat_%s", talkerMd5)

		// 构建查询条件
		query := fmt.Sprintf(`
			SELECT msgCreateTime, msgContent, messageType, mesDes
			FROM %s
			WHERE msgCreateTime >= ? AND msgCreateTime <= ?
			ORDER BY msgCreateTime ASC
		`, tableName)

		// 执行查询
		rows, err := db.QueryContext(ctx, query, startTime.Unix(), endTime.Unix())
		if err != nil {
			// 如果表不存在，跳过此talker
			if strings.Contains(err.Error(), "no such table") {
				continue
			}
			log.Err(err).Msgf("从数据库 %s 查询消息失败", dbPath)
			continue
		}

		// 处理查询结果，在读取时进行过滤
		for rows.Next() {
			var msg model.MessageDarwinV3
			err := rows.Scan(
				&msg.MsgCreateTime,
				&msg.MsgContent,
				&msg.MessageType,
				&msg.MesDes,
			)
			if err != nil {
				rows.Close()
				log.Err(err).Msgf("扫描消息行失败")
				continue
			}

			// 将消息包装为通用模型
			message := msg.Wrap(talkerItem)

			// 应用sender过滤
			if len(senders) > 0 {
				senderMatch := false
				for _, s := range senders {
					if message.Sender == s {
						senderMatch = true
						break
					}
				}
				if !senderMatch {
					continue // 不匹配sender，跳过此消息
				}
			}

			// 应用keyword过滤
			if regex != nil {
				plainText := message.PlainTextContent()
				if !regex.MatchString(plainText) {
					continue // 不匹配keyword，跳过此消息
				}
			}

			// 通过所有过滤条件，保留此消息
			filteredMessages = append(filteredMessages, message)

			// 检查是否已经满足分页处理数量
			if limit > 0 && len(filteredMessages) >= offset+limit {
				// 已经获取了足够的消息，可以提前返回
				rows.Close()

				// 对所有消息按时间排序
				sort.Slice(filteredMessages, func(i, j int) bool {
					return filteredMessages[i].Seq < filteredMessages[j].Seq
				})

				// 处理分页
				if offset >= len(filteredMessages) {
					return []*model.Message{}, nil
				}
				end := offset + limit
				if end > len(filteredMessages) {
					end = len(filteredMessages)
				}
				return filteredMessages[offset:end], nil
			}
		}
		rows.Close()
	}

	// 对所有消息按时间排序
	// FIXME 不同 talker 需要使用 Time 排序
	sort.Slice(filteredMessages, func(i, j int) bool {
		return filteredMessages[i].Time.Before(filteredMessages[j].Time)
	})

	// 处理分页
	if limit > 0 {
		if offset >= len(filteredMessages) {
			return []*model.Message{}, nil
		}
		end := offset + limit
		if end > len(filteredMessages) {
			end = len(filteredMessages)
		}
		return filteredMessages[offset:end], nil
	}

	return filteredMessages, nil
}

// 从表名中提取 talker
func extractTalkerFromTableName(tableName string) string {

	if !strings.HasPrefix(tableName, "Chat_") {
		return ""
	}

	if strings.HasSuffix(tableName, "_dels") {
		return ""
	}

	return strings.TrimPrefix(tableName, "Chat_")
}

// GetContacts 实现获取联系人信息的方法
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
				FROM WCContact
				WHERE m_nsUsrName = ? OR nickname = ? OR m_nsRemark = ? OR m_nsAliasName = ?`
		args = []interface{}{key, key, key, key}
	} else {
		// 查询所有联系人
		query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
				FROM WCContact`
	}

	// 添加排序、分页
	query += ` ORDER BY m_nsUsrName`
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Contact)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	contacts := []*model.Contact{}
	for rows.Next() {
		var contactDarwinV3 model.ContactDarwinV3
		err := rows.Scan(
			&contactDarwinV3.M_nsUsrName,
			&contactDarwinV3.Nickname,
			&contactDarwinV3.M_nsRemark,
			&contactDarwinV3.M_uiSex,
			&contactDarwinV3.M_nsAliasName,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		contacts = append(contacts, contactDarwinV3.Wrap())
	}

	return contacts, nil
}

// GetChatRooms 实现获取群聊信息的方法
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
				FROM GroupContact
				WHERE m_nsUsrName = ? OR nickname = ? OR m_nsRemark = ?`
		args = []interface{}{key, key, key}
	} else {
		// 查询所有群聊
		query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
				FROM GroupContact`
	}

	// 添加排序、分页
	query += ` ORDER BY m_nsUsrName`
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(ChatRoom)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	chatRooms := []*model.ChatRoom{}
	for rows.Next() {
		var chatRoomDarwinV3 model.ChatRoomDarwinV3
		err := rows.Scan(
			&chatRoomDarwinV3.M_nsUsrName,
			&chatRoomDarwinV3.Nickname,
			&chatRoomDarwinV3.M_nsRemark,
			&chatRoomDarwinV3.M_nsChatRoomMemList,
			&chatRoomDarwinV3.M_nsChatRoomAdminList,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
	}

	// 如果没有找到群聊，尝试通过联系人查找
	if len(chatRooms) == 0 && key != "" {
		contacts, err := ds.GetContacts(ctx, key, 1, 0)
		if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
			// 再次尝试通过用户名查找群聊
			rows, err := db.QueryContext(ctx,
				`SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
				FROM GroupContact
				WHERE m_nsUsrName = ?`,
				contacts[0].UserName)

			if err != nil {
				return nil, errors.QueryFailed(query, err)
			}
			defer rows.Close()

			for rows.Next() {
				var chatRoomDarwinV3 model.ChatRoomDarwinV3
				err := rows.Scan(
					&chatRoomDarwinV3.M_nsUsrName,
					&chatRoomDarwinV3.Nickname,
					&chatRoomDarwinV3.M_nsRemark,
					&chatRoomDarwinV3.M_nsChatRoomMemList,
					&chatRoomDarwinV3.M_nsChatRoomAdminList,
				)

				if err != nil {
					return nil, errors.ScanRowFailed(err)
				}

				chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
			}

			// 如果群聊记录不存在，但联系人记录存在，创建一个模拟的群聊对象
			if len(chatRooms) == 0 {
				chatRooms = append(chatRooms, &model.ChatRoom{
					Name:  contacts[0].UserName,
					Users: make([]model.ChatRoomUser, 0),
				})
			}
		}
	}

	return chatRooms, nil
}

// GetSessions 实现获取会话信息的方法
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
	var query string
	var args []interface{}

	if key != "" {
		// 按照关键字查询
		query = `SELECT m_nsUserName, m_uLastTime
				FROM SessionAbstract
				WHERE m_nsUserName = ?`
		args = []interface{}{key}
	} else {
		// 查询所有会话
		query = `SELECT m_nsUserName, m_uLastTime
				FROM SessionAbstract`
	}

	// 添加排序、分页
	query += ` ORDER BY m_uLastTime DESC`
	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET %d", offset)
		}
	}

	// 执行查询
	db, err := ds.dbm.GetDB(Session)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	sessions := []*model.Session{}
	for rows.Next() {
		var sessionDarwinV3 model.SessionDarwinV3
		err := rows.Scan(
			&sessionDarwinV3.M_nsUserName,
			&sessionDarwinV3.M_uLastTime,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		// 包装成通用模型
		session := sessionDarwinV3.Wrap()

		// 尝试获取联系人信息以补充会话信息
		contacts, err := ds.GetContacts(ctx, session.UserName, 1, 0)
		if err == nil && len(contacts) > 0 {
			session.NickName = contacts[0].DisplayName()
		} else {
			// 尝试获取群聊信息
			chatRooms, err := ds.GetChatRooms(ctx, session.UserName, 1, 0)
			if err == nil && len(chatRooms) > 0 {
				session.NickName = chatRooms[0].DisplayName()
			}
		}

		sessions = append(sessions, session)
	}

	return sessions, nil
}

func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
	if key == "" {
		return nil, errors.ErrKeyEmpty
	}
	query := `SELECT
    r.mediaMd5,
    r.mediaSize,
    r.inodeNumber,
    r.modifyTime,
    d.relativePath,
    d.fileName
FROM
    HlinkMediaRecord r
JOIN
    HlinkMediaDetail d ON r.inodeNumber = d.inodeNumber
WHERE
    r.mediaMd5 = ?`
	args := []interface{}{key}
	// 执行查询
	db, err := ds.dbm.GetDB(Media)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, errors.QueryFailed(query, err)
	}
	defer rows.Close()

	var media *model.Media
	for rows.Next() {
		var mediaDarwinV3 model.MediaDarwinV3
		err := rows.Scan(
			&mediaDarwinV3.MediaMd5,
			&mediaDarwinV3.MediaSize,
			&mediaDarwinV3.InodeNumber,
			&mediaDarwinV3.ModifyTime,
			&mediaDarwinV3.RelativePath,
			&mediaDarwinV3.FileName,
		)

		if err != nil {
			return nil, errors.ScanRowFailed(err)
		}

		// 包装成通用模型
		media = mediaDarwinV3.Wrap()
	}

	if media == nil {
		return nil, errors.ErrMediaNotFound
	}

	return media, nil
}

// Close 实现关闭数据库连接的方法
func (ds *DataSource) Close() error {
	return ds.dbm.Close()
}

package darwin

import (
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/shirou/gopsutil/v4/process"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/model"
	"github.com/sjzar/chatlog/pkg/appver"
)

const (
	ProcessNameOfficial = "WeChat"
	ProcessNameBeta     = "Weixin"
	V3DBFile            = "Message/msg_0.db"
	V4DBFile            = "db_storage/session/session.db"
)

// Detector 实现 macOS 平台的进程检测器
type Detector struct{}

// NewDetector 创建一个新的 macOS 检测器
func NewDetector() *Detector {
	return &Detector{}
}

// FindProcesses 查找所有微信进程并返回它们的信息
func (d *Detector) FindProcesses() ([]*model.Process, error) {
	processes, err := process.Processes()
	if err != nil {
		log.Err(err).Msg("获取进程列表失败")
		return nil, err
	}

	var result []*model.Process
	for _, p := range processes {
		name, err := p.Name()
		if err != nil || (name != ProcessNameOfficial && name != ProcessNameBeta) {
			continue
		}

		// 获取进程信息
		procInfo, err := d.getProcessInfo(p)
		if err != nil {
			log.Err(err).Msgf("获取进程 %d 的信息失败", p.Pid)
			continue
		}

		result = append(result, procInfo)
	}

	return result, nil
}

// getProcessInfo 获取微信进程的详细信息
func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
	procInfo := &model.Process{
		PID:      uint32(p.Pid),
		Status:   model.StatusOffline,
		Platform: model.PlatformMacOS,
	}

	// 获取可执行文件路径
	exePath, err := p.Exe()
	if err != nil {
		log.Err(err).Msg("获取可执行文件路径失败")
		return nil, err
	}
	procInfo.ExePath = exePath

	// 获取版本信息
	// 注意：macOS 的版本获取方式可能与 Windows 不同
	versionInfo, err := appver.New(exePath)
	if err != nil {
		log.Err(err).Msg("获取版本信息失败")
		procInfo.Version = 3
		procInfo.FullVersion = "3.0.0"
	} else {
		procInfo.Version = versionInfo.Version
		procInfo.FullVersion = versionInfo.FullVersion
	}

	// 初始化附加信息（数据目录、账户名）
	if err := d.initializeProcessInfo(p, procInfo); err != nil {
		log.Err(err).Msg("初始化进程信息失败")
		// 即使初始化失败也返回部分信息
	}

	return procInfo, nil
}

// initializeProcessInfo 获取进程的数据目录和账户名
func (d *Detector) initializeProcessInfo(p *process.Process, info *model.Process) error {
	// 使用 lsof 命令获取进程打开的文件
	files, err := d.getOpenFiles(int(p.Pid))
	if err != nil {
		log.Err(err).Msg("获取打开的文件失败")
		return err
	}

	dbPath := V3DBFile
	if info.Version == 4 {
		dbPath = V4DBFile
	}

	for _, filePath := range files {
		if strings.Contains(filePath, dbPath) {
			parts := strings.Split(filePath, string(filepath.Separator))
			if len(parts) < 4 {
				log.Debug().Msg("无效的文件路径格式: " + filePath)
				continue
			}

			// v3:
			// /Users/sarv/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/<id>/Message/msg_0.db
			// v4:
			// /Users/sarv/Library/Containers/com.tencent.xWeChat/Data/Documents/xwechat_files/<id>/db_storage/message/message_0.db

			info.Status = model.StatusOnline
			if info.Version == 4 {
				info.DataDir = strings.Join(parts[:len(parts)-3], string(filepath.Separator))
				info.AccountName = parts[len(parts)-4]
			} else {
				info.DataDir = strings.Join(parts[:len(parts)-2], string(filepath.Separator))
				info.AccountName = parts[len(parts)-3]
			}
			return nil
		}
	}

	return nil
}

// getOpenFiles 使用 lsof 命令获取进程打开的文件列表
func (d *Detector) getOpenFiles(pid int) ([]string, error) {
	// 执行 lsof -p <pid> 命令，使用 -F n 选项只输出文件名
	cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-F", "n")
	output, err := cmd.Output()
	if err != nil {
		return nil, errors.RunCmdFailed(err)
	}

	// 解析 lsof -F n 输出
	// 格式为: n/path/to/file
	lines := strings.Split(string(output), "\n")
	var files []string

	for _, line := range lines {
		if strings.HasPrefix(line, "n") {
			// 移除前缀 'n' 获取文件路径
			filePath := line[1:]
			if filePath != "" {
				files = append(files, filePath)
			}
		}
	}

	return files, nil
}

package chatlog

import (
	"archive/zip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/sjzar/chatlog/internal/wechat"
	"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
)

func init() {
	rootCmd.AddCommand(dumpmemoryCmd)
}

var dumpmemoryCmd = &cobra.Command{
	Use:   "dumpmemory",
	Short: "dump memory",
	Run: func(cmd *cobra.Command, args []string) {
		if runtime.GOOS != "darwin" {
			log.Info().Msg("dump memory only support macOS")
		}

		session := time.Now().Format("20060102150405")

		dir, err := os.Getwd()
		if err != nil {
			log.Fatal().Err(err).Msg("get current directory failed")
			return
		}
		log.Info().Msgf("current directory: %s", dir)

		// step 1. check pid
		if err = wechat.Load(); err != nil {
			log.Fatal().Err(err).Msg("load wechat failed")
			return
		}
		accounts := wechat.GetAccounts()
		if len(accounts) == 0 {
			log.Fatal().Msg("no wechat account found")
			return
		}

		log.Info().Msgf("found %d wechat account", len(accounts))
		for i, a := range accounts {
			log.Info().Msgf("%d. %s %d %s", i, a.FullVersion, a.PID, a.DataDir)
		}

		// step 2. dump memory
		account := accounts[0]
		file := fmt.Sprintf("wechat_%s_%d_%s.bin", account.FullVersion, account.PID, session)
		path := filepath.Join(dir, file)
		log.Info().Msgf("dumping memory to %s", path)

		g := glance.NewGlance(account.PID)
		b, err := g.Read()
		if err != nil {
			log.Fatal().Err(err).Msg("read memory failed")
			return
		}

		if err = os.WriteFile(path, b, 0644); err != nil {
			log.Fatal().Err(err).Msg("write memory failed")
			return
		}

		log.Info().Msg("dump memory success")

		// step 3. copy encrypted database file
		dbFile := "db_storage/session/session.db"
		if account.Version == 3 {
			dbFile = "Session/session_new.db"
		}
		from := filepath.Join(account.DataDir, dbFile)
		to := filepath.Join(dir, fmt.Sprintf("wechat_%s_%d_session.db", account.FullVersion, account.PID))

		log.Info().Msgf("copying %s to %s", from, to)
		b, err = os.ReadFile(from)
		if err != nil {
			log.Fatal().Err(err).Msg("read session.db failed")
			return
		}
		if err = os.WriteFile(to, b, 0644); err != nil {
			log.Fatal().Err(err).Msg("write session.db failed")
			return
		}
		log.Info().Msg("copy session.db success")

		// step 4. package
		zipFile := fmt.Sprintf("wechat_%s_%d_%s.zip", account.FullVersion, account.PID, session)
		zipPath := filepath.Join(dir, zipFile)
		log.Info().Msgf("packaging to %s", zipPath)

		zf, err := os.Create(zipPath)
		if err != nil {
			log.Fatal().Err(err).Msg("create zip file failed")
			return
		}
		defer zf.Close()

		zw := zip.NewWriter(zf)

		for _, file := range []string{file, to} {
			f, err := os.Open(file)
			if err != nil {
				log.Fatal().Err(err).Msg("open file failed")
				return
			}
			defer f.Close()
			info, err := f.Stat()
			if err != nil {
				log.Fatal().Err(err).Msg("get file info failed")
				return
			}
			header, err := zip.FileInfoHeader(info)
			if err != nil {
				log.Fatal().Err(err).Msg("create zip file info header failed")
				return
			}
			header.Name = filepath.Base(file)
			header.Method = zip.Deflate
			writer, err := zw.CreateHeader(header)
			if err != nil {
				log.Fatal().Err(err).Msg("create zip file header failed")
				return
			}
			if _, err = io.Copy(writer, f); err != nil {
				log.Fatal().Err(err).Msg("copy file to zip failed")
				return
			}
		}
		if err = zw.Close(); err != nil {
			log.Fatal().Err(err).Msg("close zip writer failed")
			return
		}

		log.Info().Msgf("package success, please send %s to developer", zipPath)
	},
}

package model

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"github.com/sjzar/chatlog/internal/model/wxproto"
	"github.com/sjzar/chatlog/pkg/util/zstd"
	"google.golang.org/protobuf/proto"
)

// CREATE TABLE Msg_md5(talker)(
// local_id INTEGER PRIMARY KEY AUTOINCREMENT,
// server_id INTEGER,
// local_type INTEGER,
// sort_seq INTEGER,
// real_sender_id INTEGER,
// create_time INTEGER,
// status INTEGER,
// upload_status INTEGER,
// download_status INTEGER,
// server_seq INTEGER,
// origin_source INTEGER,
// source TEXT,
// message_content TEXT,
// compress_content TEXT,
// packed_info_data BLOB,
// WCDB_CT_message_content INTEGER DEFAULT NULL,
// WCDB_CT_source INTEGER DEFAULT NULL
// )
type MessageV4 struct {
	SortSeq        int64  `json:"sort_seq"`         // 消息序号，10位时间戳 + 3位序号
	ServerID       int64  `json:"server_id"`        // 消息 ID，用于关联 voice
	LocalType      int64  `json:"local_type"`       // 消息类型
	UserName       string `json:"user_name"`        // 发送人，通过 Join Name2Id 表获得
	CreateTime     int64  `json:"create_time"`      // 消息创建时间，10位时间戳
	MessageContent []byte `json:"message_content"`  // 消息内容，文字聊天内容 或 zstd 压缩内容
	PackedInfoData []byte `json:"packed_info_data"` // 额外数据，类似 proto，格式与 v3 有差异
	Status         int    `json:"status"`           // 消息状态，2 是已发送，4 是已接收，可以用于判断 IsSender（FIXME 不准, 需要判断 UserName）
}

func (m *MessageV4) Wrap(talker string) *Message {

	_m := &Message{
		Seq:        m.SortSeq,
		Time:       time.Unix(m.CreateTime, 0),
		Talker:     talker,
		IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
		Sender:     m.UserName,
		Type:       m.LocalType,
		Contents:   make(map[string]interface{}),
		Version:    WeChatV4,
	}

	// FIXME 后续通过 UserName 判断是否是自己发送的消息，目前可能不准确
	_m.IsSelf = m.Status == 2 || (!_m.IsChatRoom && talker != m.UserName)

	content := ""
	if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
		if b, err := zstd.Decompress(m.MessageContent); err == nil {
			content = string(b)
		}
	} else {
		content = string(m.MessageContent)
	}

	if _m.IsChatRoom {
		split := strings.SplitN(content, ":\n", 2)
		if len(split) == 2 {
			_m.Sender = split[0]
			content = split[1]
		}
	}

	_m.ParseMediaInfo(content)

	// 语音消息
	if _m.Type == 34 {
		_m.Contents["voice"] = fmt.Sprint(m.ServerID)
	}

	if len(m.PackedInfoData) != 0 {
		if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil {
			// FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题
			if _m.Type == 3 && packedInfo.Image != nil {
				_talkerMd5Bytes := md5.Sum([]byte(talker))
				talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
				_m.Contents["imgfile"] = filepath.Join("msg", "attach", talkerMd5, _m.Time.Format("2006-01"), "Img", fmt.Sprintf("%s.dat", packedInfo.Image.Md5))
				_m.Contents["thumb"] = filepath.Join("msg", "attach", talkerMd5, _m.Time.Format("2006-01"), "Img", fmt.Sprintf("%s_t.dat", packedInfo.Image.Md5))
			}
			if _m.Type == 43 && packedInfo.Video != nil {
				_m.Contents["videofile"] = filepath.Join("msg", "video", _m.Time.Format("2006-01"), fmt.Sprintf("%s.mp4", packedInfo.Video.Md5))
				_m.Contents["thumb"] = filepath.Join("msg", "video", _m.Time.Format("2006-01"), fmt.Sprintf("%s_thumb.jpg", packedInfo.Video.Md5))
			}
		}
	}

	return _m
}

func ParsePackedInfo(b []byte) *wxproto.PackedInfo {
	var pbMsg wxproto.PackedInfo
	if err := proto.Unmarshal(b, &pbMsg); err != nil {
		return nil
	}
	return &pbMsg
}

package common

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"encoding/binary"
	"fmt"
	"hash"
	"io"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
)

const (
	KeySize      = 32
	SaltSize     = 16
	AESBlockSize = 16
	SQLiteHeader = "SQLite format 3\x00"
	IVSize       = 16
)

type DBFile struct {
	Path       string
	Salt       []byte
	TotalPages int64
	FirstPage  []byte
}

func OpenDBFile(dbPath string, pageSize int) (*DBFile, error) {
	fp, err := os.Open(dbPath)
	if err != nil {
		return nil, errors.OpenFileFailed(dbPath, err)
	}
	defer fp.Close()

	fileInfo, err := fp.Stat()
	if err != nil {
		return nil, errors.StatFileFailed(dbPath, err)
	}

	fileSize := fileInfo.Size()
	totalPages := fileSize / int64(pageSize)
	if fileSize%int64(pageSize) > 0 {
		totalPages++
	}

	buffer := make([]byte, pageSize)
	n, err := io.ReadFull(fp, buffer)
	if err != nil {
		return nil, errors.ReadFileFailed(dbPath, err)
	}
	if n != pageSize {
		return nil, errors.IncompleteRead(fmt.Errorf("read %d bytes, expected %d", n, pageSize))
	}

	if bytes.Equal(buffer[:len(SQLiteHeader)-1], []byte(SQLiteHeader[:len(SQLiteHeader)-1])) {
		return nil, errors.ErrAlreadyDecrypted
	}

	return &DBFile{
		Path:       dbPath,
		Salt:       buffer[:SaltSize],
		FirstPage:  buffer,
		TotalPages: totalPages,
	}, nil
}

func XorBytes(a []byte, b byte) []byte {
	result := make([]byte, len(a))
	for i := range a {
		result[i] = a[i] ^ b
	}
	return result
}

func ValidateKey(page1 []byte, key []byte, salt []byte, hashFunc func() hash.Hash, hmacSize int, reserve int, pageSize int, deriveKeys func([]byte, []byte) ([]byte, []byte)) bool {
	if len(key) != KeySize {
		return false
	}

	_, macKey := deriveKeys(key, salt)

	mac := hmac.New(hashFunc, macKey)
	dataEnd := pageSize - reserve + IVSize
	mac.Write(page1[SaltSize:dataEnd])

	pageNoBytes := make([]byte, 4)
	binary.LittleEndian.PutUint32(pageNoBytes, 1)
	mac.Write(pageNoBytes)

	calculatedMAC := mac.Sum(nil)
	storedMAC := page1[dataEnd : dataEnd+hmacSize]

	return hmac.Equal(calculatedMAC, storedMAC)
}

func DecryptPage(pageBuf []byte, encKey []byte, macKey []byte, pageNum int64, hashFunc func() hash.Hash, hmacSize int, reserve int, pageSize int) ([]byte, error) {
	offset := 0
	if pageNum == 0 {
		offset = SaltSize
	}

	mac := hmac.New(hashFunc, macKey)
	mac.Write(pageBuf[offset : pageSize-reserve+IVSize])

	pageNoBytes := make([]byte, 4)
	binary.LittleEndian.PutUint32(pageNoBytes, uint32(pageNum+1))
	mac.Write(pageNoBytes)

	hashMac := mac.Sum(nil)

	hashMacStartOffset := pageSize - reserve + IVSize
	hashMacEndOffset := hashMacStartOffset + hmacSize

	if !bytes.Equal(hashMac, pageBuf[hashMacStartOffset:hashMacEndOffset]) {
		return nil, errors.ErrDecryptHashVerificationFailed
	}

	iv := pageBuf[pageSize-reserve : pageSize-reserve+IVSize]
	block, err := aes.NewCipher(encKey)
	if err != nil {
		return nil, errors.DecryptCreateCipherFailed(err)
	}

	mode := cipher.NewCBCDecrypter(block, iv)

	encrypted := make([]byte, pageSize-reserve-offset)
	copy(encrypted, pageBuf[offset:pageSize-reserve])

	mode.CryptBlocks(encrypted, encrypted)

	decryptedPage := append(encrypted, pageBuf[pageSize-reserve:pageSize]...)

	return decryptedPage, nil
}

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.36.5
// 	protoc        v5.29.3
// source: packedinfo.proto

package wxproto

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type PackedInfo struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Type          uint32                 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"`       // 始终为 106 (0x6a)
	Version       uint32                 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // 始终为 14 (0xe)
	Image         *ImageHash             `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`      // 图片哈希
	Video         *VideoHash             `protobuf:"bytes,4,opt,name=video,proto3" json:"video,omitempty"`      // 视频哈希
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *PackedInfo) Reset() {
	*x = PackedInfo{}
	mi := &file_packedinfo_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *PackedInfo) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*PackedInfo) ProtoMessage() {}

func (x *PackedInfo) ProtoReflect() protoreflect.Message {
	mi := &file_packedinfo_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use PackedInfo.ProtoReflect.Descriptor instead.
func (*PackedInfo) Descriptor() ([]byte, []int) {
	return file_packedinfo_proto_rawDescGZIP(), []int{0}
}

func (x *PackedInfo) GetType() uint32 {
	if x != nil {
		return x.Type
	}
	return 0
}

func (x *PackedInfo) GetVersion() uint32 {
	if x != nil {
		return x.Version
	}
	return 0
}

func (x *PackedInfo) GetImage() *ImageHash {
	if x != nil {
		return x.Image
	}
	return nil
}

func (x *PackedInfo) GetVideo() *VideoHash {
	if x != nil {
		return x.Video
	}
	return nil
}

type ImageHash struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Md5           string                 `protobuf:"bytes,4,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *ImageHash) Reset() {
	*x = ImageHash{}
	mi := &file_packedinfo_proto_msgTypes[1]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *ImageHash) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*ImageHash) ProtoMessage() {}

func (x *ImageHash) ProtoReflect() protoreflect.Message {
	mi := &file_packedinfo_proto_msgTypes[1]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use ImageHash.ProtoReflect.Descriptor instead.
func (*ImageHash) Descriptor() ([]byte, []int) {
	return file_packedinfo_proto_rawDescGZIP(), []int{1}
}

func (x *ImageHash) GetMd5() string {
	if x != nil {
		return x.Md5
	}
	return ""
}

type VideoHash struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Md5           string                 `protobuf:"bytes,8,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *VideoHash) Reset() {
	*x = VideoHash{}
	mi := &file_packedinfo_proto_msgTypes[2]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *VideoHash) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*VideoHash) ProtoMessage() {}

func (x *VideoHash) ProtoReflect() protoreflect.Message {
	mi := &file_packedinfo_proto_msgTypes[2]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use VideoHash.ProtoReflect.Descriptor instead.
func (*VideoHash) Descriptor() ([]byte, []int) {
	return file_packedinfo_proto_rawDescGZIP(), []int{2}
}

func (x *VideoHash) GetMd5() string {
	if x != nil {
		return x.Md5
	}
	return ""
}

var File_packedinfo_proto protoreflect.FileDescriptor

var file_packedinfo_proto_rawDesc = string([]byte{
	0x0a, 0x10, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f,
	0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
	0x22, 0x98, 0x01, 0x0a, 0x0a, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12,
	0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74,
	0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
	0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a,
	0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61,
	0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6d, 0x61, 0x67,
	0x65, 0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x05,
	0x76, 0x69, 0x64, 0x65, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x70,
	0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f,
	0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x22, 0x1d, 0x0a, 0x09, 0x49,
	0x6d, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18,
	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x22, 0x1d, 0x0a, 0x09, 0x56, 0x69,
	0x64, 0x65, 0x6f, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18, 0x08,
	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b, 0x77,
	0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})

var (
	file_packedinfo_proto_rawDescOnce sync.Once
	file_packedinfo_proto_rawDescData []byte
)

func file_packedinfo_proto_rawDescGZIP() []byte {
	file_packedinfo_proto_rawDescOnce.Do(func() {
		file_packedinfo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)))
	})
	return file_packedinfo_proto_rawDescData
}

var file_packedinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_packedinfo_proto_goTypes = []any{
	(*PackedInfo)(nil), // 0: app.protobuf.PackedInfo
	(*ImageHash)(nil),  // 1: app.protobuf.ImageHash
	(*VideoHash)(nil),  // 2: app.protobuf.VideoHash
}
var file_packedinfo_proto_depIdxs = []int32{
	1, // 0: app.protobuf.PackedInfo.image:type_name -> app.protobuf.ImageHash
	2, // 1: app.protobuf.PackedInfo.video:type_name -> app.protobuf.VideoHash
	2, // [2:2] is the sub-list for method output_type
	2, // [2:2] is the sub-list for method input_type
	2, // [2:2] is the sub-list for extension type_name
	2, // [2:2] is the sub-list for extension extendee
	0, // [0:2] is the sub-list for field type_name
}

func init() { file_packedinfo_proto_init() }
func file_packedinfo_proto_init() {
	if File_packedinfo_proto != nil {
		return
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)),
			NumEnums:      0,
			NumMessages:   3,
			NumExtensions: 0,
			NumServices:   0,
		},
		GoTypes:           file_packedinfo_proto_goTypes,
		DependencyIndexes: file_packedinfo_proto_depIdxs,
		MessageInfos:      file_packedinfo_proto_msgTypes,
	}.Build()
	File_packedinfo_proto = out.File
	file_packedinfo_proto_goTypes = nil
	file_packedinfo_proto_depIdxs = nil
}

// v3 & v4 通用，可能会有部分字段差异

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.36.5
// 	protoc        v5.29.3
// source: roomdata.proto

package wxproto

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type RoomData struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Users         []*RoomDataUser        `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
	RoomCap       *int32                 `protobuf:"varint,5,opt,name=roomCap,proto3,oneof" json:"roomCap,omitempty"` // 只在第一份数据中出现，值为500
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *RoomData) Reset() {
	*x = RoomData{}
	mi := &file_roomdata_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *RoomData) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*RoomData) ProtoMessage() {}

func (x *RoomData) ProtoReflect() protoreflect.Message {
	mi := &file_roomdata_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use RoomData.ProtoReflect.Descriptor instead.
func (*RoomData) Descriptor() ([]byte, []int) {
	return file_roomdata_proto_rawDescGZIP(), []int{0}
}

func (x *RoomData) GetUsers() []*RoomDataUser {
	if x != nil {
		return x.Users
	}
	return nil
}

func (x *RoomData) GetRoomCap() int32 {
	if x != nil && x.RoomCap != nil {
		return *x.RoomCap
	}
	return 0
}

type RoomDataUser struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	UserName      string                 `protobuf:"bytes,1,opt,name=userName,proto3" json:"userName,omitempty"`             // 用户ID或名称
	DisplayName   *string                `protobuf:"bytes,2,opt,name=displayName,proto3,oneof" json:"displayName,omitempty"` // 显示名称，可能是UTF-8编码的中文，部分记录可能为空
	Status        int32                  `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"`                // 状态码，值范围0-9
	Inviter       *string                `protobuf:"bytes,4,opt,name=inviter,proto3,oneof" json:"inviter,omitempty"`         // 邀请人
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *RoomDataUser) Reset() {
	*x = RoomDataUser{}
	mi := &file_roomdata_proto_msgTypes[1]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *RoomDataUser) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*RoomDataUser) ProtoMessage() {}

func (x *RoomDataUser) ProtoReflect() protoreflect.Message {
	mi := &file_roomdata_proto_msgTypes[1]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use RoomDataUser.ProtoReflect.Descriptor instead.
func (*RoomDataUser) Descriptor() ([]byte, []int) {
	return file_roomdata_proto_rawDescGZIP(), []int{1}
}

func (x *RoomDataUser) GetUserName() string {
	if x != nil {
		return x.UserName
	}
	return ""
}

func (x *RoomDataUser) GetDisplayName() string {
	if x != nil && x.DisplayName != nil {
		return *x.DisplayName
	}
	return ""
}

func (x *RoomDataUser) GetStatus() int32 {
	if x != nil {
		return x.Status
	}
	return 0
}

func (x *RoomDataUser) GetInviter() string {
	if x != nil && x.Inviter != nil {
		return *x.Inviter
	}
	return ""
}

var File_roomdata_proto protoreflect.FileDescriptor

var file_roomdata_proto_rawDesc = string([]byte{
	0x0a, 0x0e, 0x72, 0x6f, 0x6f, 0x6d, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
	0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x67,
	0x0a, 0x08, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x73,
	0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x70, 0x2e,
	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74,
	0x61, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x07,
	0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52,
	0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
	0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x22, 0xa4, 0x01, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d,
	0x44, 0x61, 0x74, 0x61, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72,
	0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72,
	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e,
	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x69, 0x73,
	0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x16, 0x0a, 0x06, 0x73,
	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61,
	0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x18, 0x04,
	0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x88,
	0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61,
	0x6d, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x42, 0x0b,
	0x5a, 0x09, 0x2e, 0x3b, 0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
	0x74, 0x6f, 0x33,
})

var (
	file_roomdata_proto_rawDescOnce sync.Once
	file_roomdata_proto_rawDescData []byte
)

func file_roomdata_proto_rawDescGZIP() []byte {
	file_roomdata_proto_rawDescOnce.Do(func() {
		file_roomdata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)))
	})
	return file_roomdata_proto_rawDescData
}

var file_roomdata_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_roomdata_proto_goTypes = []any{
	(*RoomData)(nil),     // 0: app.protobuf.RoomData
	(*RoomDataUser)(nil), // 1: app.protobuf.RoomDataUser
}
var file_roomdata_proto_depIdxs = []int32{
	1, // 0: app.protobuf.RoomData.users:type_name -> app.protobuf.RoomDataUser
	1, // [1:1] is the sub-list for method output_type
	1, // [1:1] is the sub-list for method input_type
	1, // [1:1] is the sub-list for extension type_name
	1, // [1:1] is the sub-list for extension extendee
	0, // [0:1] is the sub-list for field type_name
}

func init() { file_roomdata_proto_init() }
func file_roomdata_proto_init() {
	if File_roomdata_proto != nil {
		return
	}
	file_roomdata_proto_msgTypes[0].OneofWrappers = []any{}
	file_roomdata_proto_msgTypes[1].OneofWrappers = []any{}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)),
			NumEnums:      0,
			NumMessages:   2,
			NumExtensions: 0,
			NumServices:   0,
		},
		GoTypes:           file_roomdata_proto_goTypes,
		DependencyIndexes: file_roomdata_proto_depIdxs,
		MessageInfos:      file_roomdata_proto_msgTypes,
	}.Build()
	File_roomdata_proto = out.File
	file_roomdata_proto_goTypes = nil
	file_roomdata_proto_depIdxs = nil
}

package wechat

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat"
	"github.com/sjzar/chatlog/internal/wechat/decrypt"
	"github.com/sjzar/chatlog/pkg/filemonitor"
	"github.com/sjzar/chatlog/pkg/util"
)

var (
	DebounceTime = 1 * time.Second
	MaxWaitTime  = 10 * time.Second
)

type Service struct {
	ctx            *ctx.Context
	lastEvents     map[string]time.Time
	pendingActions map[string]bool
	mutex          sync.Mutex
	fm             *filemonitor.FileMonitor
}

func NewService(ctx *ctx.Context) *Service {
	return &Service{
		ctx:            ctx,
		lastEvents:     make(map[string]time.Time),
		pendingActions: make(map[string]bool),
	}
}

// GetWeChatInstances returns all running WeChat instances
func (s *Service) GetWeChatInstances() []*wechat.Account {
	wechat.Load()
	return wechat.GetAccounts()
}

// GetDataKey extracts the encryption key from a WeChat process
func (s *Service) GetDataKey(info *wechat.Account) (string, error) {
	if info == nil {
		return "", fmt.Errorf("no WeChat instance selected")
	}

	key, err := info.GetKey(context.Background())
	if err != nil {
		return "", err
	}

	return key, nil
}

func (s *Service) StartAutoDecrypt() error {
	dbGroup, err := filemonitor.NewFileGroup("wechat", s.ctx.DataDir, `.*\.db$`, []string{"fts"})
	if err != nil {
		return err
	}
	dbGroup.AddCallback(s.DecryptFileCallback)

	s.fm = filemonitor.NewFileMonitor()
	s.fm.AddGroup(dbGroup)
	if err := s.fm.Start(); err != nil {
		log.Debug().Err(err).Msg("failed to start file monitor")
		return err
	}
	return nil
}

func (s *Service) StopAutoDecrypt() error {
	if s.fm != nil {
		if err := s.fm.Stop(); err != nil {
			return err
		}
	}
	s.fm = nil
	return nil
}

func (s *Service) DecryptFileCallback(event fsnotify.Event) error {
	if event.Op.Has(fsnotify.Chmod) || !event.Op.Has(fsnotify.Write) {
		return nil
	}

	s.mutex.Lock()
	s.lastEvents[event.Name] = time.Now()

	if !s.pendingActions[event.Name] {
		s.pendingActions[event.Name] = true
		s.mutex.Unlock()
		go s.waitAndProcess(event.Name)
	} else {
		s.mutex.Unlock()
	}

	return nil
}

func (s *Service) waitAndProcess(dbFile string) {
	start := time.Now()
	for {
		time.Sleep(DebounceTime)

		s.mutex.Lock()
		lastEventTime := s.lastEvents[dbFile]
		elapsed := time.Since(lastEventTime)
		totalElapsed := time.Since(start)

		if elapsed >= DebounceTime || totalElapsed >= MaxWaitTime {
			s.pendingActions[dbFile] = false
			s.mutex.Unlock()

			log.Debug().Msgf("Processing file: %s", dbFile)
			s.DecryptDBFile(dbFile)
			return
		}
		s.mutex.Unlock()
	}
}

func (s *Service) DecryptDBFile(dbFile string) error {

	decryptor, err := decrypt.NewDecryptor(s.ctx.Platform, s.ctx.Version)
	if err != nil {
		return err
	}

	output := filepath.Join(s.ctx.WorkDir, dbFile[len(s.ctx.DataDir):])
	if err := util.PrepareDir(filepath.Dir(output)); err != nil {
		return err
	}

	outputTemp := output + ".tmp"
	outputFile, err := os.Create(outputTemp)
	if err != nil {
		return fmt.Errorf("failed to create output file: %v", err)
	}
	defer func() {
		outputFile.Close()
		if err := os.Rename(outputTemp, output); err != nil {
			log.Debug().Err(err).Msgf("failed to rename %s to %s", outputTemp, output)
		}
	}()

	if err := decryptor.Decrypt(context.Background(), dbFile, s.ctx.DataKey, outputFile); err != nil {
		if err == errors.ErrAlreadyDecrypted {
			if data, err := os.ReadFile(dbFile); err == nil {
				outputFile.Write(data)
			}
			return nil
		}
		log.Err(err).Msgf("failed to decrypt %s", dbFile)
		return err
	}

	log.Debug().Msgf("Decrypted %s to %s", dbFile, output)

	return nil
}

func (s *Service) DecryptDBFiles() error {
	dbGroup, err := filemonitor.NewFileGroup("wechat", s.ctx.DataDir, `.*\.db$`, []string{"fts"})
	if err != nil {
		return err
	}

	dbFiles, err := dbGroup.List()
	if err != nil {
		return err
	}

	for _, dbFile := range dbFiles {
		if err := s.DecryptDBFile(dbFile); err != nil {
			log.Debug().Msgf("DecryptDBFile %s failed: %v", dbFile, err)
			continue
		}
	}

	return nil
}

package model

import (
	"encoding/xml"
	"fmt"
	"strings"
	"time"

	"github.com/sjzar/chatlog/pkg/util"
)

var Debug = false

const (
	WeChatV3       = "wechatv3"
	WeChatV4       = "wechatv4"
	WeChatDarwinV3 = "wechatdarwinv3"
)

type Message struct {
	Version    string                 `json:"-"`                  // 消息版本，内部判断
	Seq        int64                  `json:"seq"`                // 消息序号，10位时间戳 + 3位序号
	Time       time.Time              `json:"time"`               // 消息创建时间，10位时间戳
	Talker     string                 `json:"talker"`             // 聊天对象，微信 ID or 群 ID
	TalkerName string                 `json:"talkerName"`         // 聊天对象名称
	IsChatRoom bool                   `json:"isChatRoom"`         // 是否为群聊消息
	Sender     string                 `json:"sender"`             // 发送人，微信 ID
	SenderName string                 `json:"senderName"`         // 发送人名称
	IsSelf     bool                   `json:"isSelf"`             // 是否为自己发送的消息
	Type       int64                  `json:"type"`               // 消息类型
	SubType    int64                  `json:"subType"`            // 消息子类型
	Content    string                 `json:"content"`            // 消息内容，文字聊天内容
	Contents   map[string]interface{} `json:"contents,omitempty"` // 消息内容，多媒体消息，采用更灵活的记录方式

	// Debug Info
	MediaMsg *MediaMsg `json:"mediaMsg,omitempty"` // 原始多媒体消息，XML 格式
	SysMsg   *SysMsg   `json:"sysMsg,omitempty"`   // 原始系统消息，XML 格式
}

func (m *Message) ParseMediaInfo(data string) error {

	m.Type, m.SubType = util.SplitInt64ToTwoInt32(m.Type)

	if m.Type == 1 {
		m.Content = data
		return nil
	}

	if m.Type == 10000 {
		var sysMsg SysMsg
		if err := xml.Unmarshal([]byte(data), &sysMsg); err != nil {
			m.Content = data
			return nil
		}
		if Debug {
			m.SysMsg = &sysMsg
		}
		m.Sender = "系统消息"
		m.SenderName = ""
		m.Content = sysMsg.String()
		return nil
	}

	var msg MediaMsg
	err := xml.Unmarshal([]byte(data), &msg)
	if err != nil {
		return err
	}

	if m.Contents == nil {
		m.Contents = make(map[string]interface{})
	}

	if Debug {
		m.MediaMsg = &msg
	}

	switch m.Type {
	case 3:
		m.Contents["md5"] = msg.Image.MD5
	case 43:
		if msg.Video.Md5 != "" {
			m.Contents["md5"] = msg.Video.Md5
		}
		if msg.Video.RawMd5 != "" {
			m.Contents["rawmd5"] = msg.Video.RawMd5
		}
	case 49:
		m.SubType = int64(msg.App.Type)
		switch m.SubType {
		case 5:
			// 链接
			m.Contents["title"] = msg.App.Title
			m.Contents["url"] = msg.App.URL
		case 6:
			// 文件
			m.Contents["title"] = msg.App.Title
			m.Contents["md5"] = msg.App.MD5
		case 19:
			// 合并转发
			m.Contents["title"] = msg.App.Title
			m.Contents["desc"] = msg.App.Des
			if msg.App.RecordItem == nil {
				break
			}
			recordInfo := &RecordInfo{}
			err := xml.Unmarshal([]byte(msg.App.RecordItem.CDATA), recordInfo)
			if err != nil {
				return err
			}
			m.Contents["recordInfo"] = recordInfo
		case 33, 36:
			// 小程序
			m.Contents["title"] = msg.App.SourceDisplayName
			m.Contents["url"] = msg.App.URL
		case 51:
			// 视频号
			if msg.App.FinderFeed == nil {
				break
			}
			m.Contents["title"] = msg.App.FinderFeed.Desc
			if len(msg.App.FinderFeed.MediaList.Media) > 0 {
				m.Contents["url"] = msg.App.FinderFeed.MediaList.Media[0].URL
			}
		case 57:
			// 引用
			m.Content = msg.App.Title
			if msg.App.ReferMsg == nil {
				break
			}
			subMsg := &Message{
				Type:       int64(msg.App.ReferMsg.Type),
				Time:       time.Unix(msg.App.ReferMsg.CreateTime, 0),
				Sender:     msg.App.ReferMsg.ChatUsr,
				SenderName: msg.App.ReferMsg.DisplayName,
			}
			if subMsg.Sender == "" {
				subMsg.Sender = msg.App.ReferMsg.FromUsr
			}
			if err := subMsg.ParseMediaInfo(msg.App.ReferMsg.Content); err != nil {
				break
			}
			m.Contents["refer"] = subMsg
		case 62:
			// 拍一拍
			if msg.App.PatMsg == nil {
				break
			}
			if len(msg.App.PatMsg.Records.Record) == 0 {
				break
			}
			m.Sender = msg.App.PatMsg.Records.Record[0].FromUser
			m.Content = msg.App.PatMsg.Records.Record[0].Templete
		case 2000:
			// 微信转账
			if msg.App.WCPayInfo == nil {
				break
			}
			// 1 实时转账
			// 3 实时转账收钱回执
			// 4 转账退还回执
			// 5 非实时转账收钱回执
			// 7 非实时转账
			_type := ""
			switch msg.App.WCPayInfo.PaySubType {
			case 1, 7:
				_type = "发送 "
			case 3, 5:
				_type = "接收 "
			case 4:
				_type = "退还 "
			}
			payMemo := ""
			if len(msg.App.WCPayInfo.PayMemo) > 0 {
				payMemo = "(" + msg.App.WCPayInfo.PayMemo + ")"
			}
			m.Content = fmt.Sprintf("[转账|%s%s]%s", _type, msg.App.WCPayInfo.FeeDesc, payMemo)
		}
	}

	return nil
}

func (m *Message) SetContent(key string, value interface{}) {
	if m.Contents == nil {
		m.Contents = make(map[string]interface{})
	}
	m.Contents[key] = value
}

func (m *Message) PlainText(showChatRoom bool, timeFormat string, host string) string {

	if timeFormat == "" {
		timeFormat = "01-02 15:04:05"
	}

	m.SetContent("host", host)

	buf := strings.Builder{}

	sender := m.Sender
	if m.IsSelf {
		sender = "我"
	}
	if m.SenderName != "" {
		buf.WriteString(m.SenderName)
		buf.WriteString("(")
		buf.WriteString(sender)
		buf.WriteString(")")
	} else {
		buf.WriteString(sender)
	}
	buf.WriteString(" ")

	if m.IsChatRoom && showChatRoom {
		buf.WriteString("[")
		if m.TalkerName != "" {
			buf.WriteString(m.TalkerName)
			buf.WriteString("(")
			buf.WriteString(m.Talker)
			buf.WriteString(")")
		} else {
			buf.WriteString(m.Talker)
		}
		buf.WriteString("] ")
	}

	buf.WriteString(m.Time.Format(timeFormat))
	buf.WriteString("\n")

	buf.WriteString(m.PlainTextContent())
	buf.WriteString("\n")

	return buf.String()
}

func (m *Message) PlainTextContent() string {
	switch m.Type {
	case 1:
		return m.Content
	case 3:
		keylist := make([]string, 0)
		if m.Contents["md5"] != nil {
			if md5, ok := m.Contents["md5"].(string); ok {
				keylist = append(keylist, md5)
			}
		}
		if m.Contents["imgfile"] != nil {
			if imgfile, ok := m.Contents["imgfile"].(string); ok {
				keylist = append(keylist, imgfile)
			}
		}
		if m.Contents["thumb"] != nil {
			if thumb, ok := m.Contents["thumb"].(string); ok {
				keylist = append(keylist, thumb)
			}
		}
		return fmt.Sprintf("![图片](http://%s/image/%s)", m.Contents["host"], strings.Join(keylist, ","))
	case 34:
		if voice, ok := m.Contents["voice"]; ok {
			return fmt.Sprintf("[语音](http://%s/voice/%s)", m.Contents["host"], voice)
		}
		return "[语音]"
	case 42:
		return "[名片]"
	case 43:
		keylist := make([]string, 0)
		if m.Contents["md5"] != nil {
			if md5, ok := m.Contents["md5"].(string); ok {
				keylist = append(keylist, md5)
			}
		}
		if m.Contents["rawmd5"] != nil {
			if rawmd5, ok := m.Contents["rawmd5"].(string); ok {
				keylist = append(keylist, rawmd5)
			}
		}
		if m.Contents["videofile"] != nil {
			if videofile, ok := m.Contents["videofile"].(string); ok {
				keylist = append(keylist, videofile)
			}
		}
		if m.Contents["thumb"] != nil {
			if thumb, ok := m.Contents["thumb"].(string); ok {
				keylist = append(keylist, thumb)
			}
		}
		return fmt.Sprintf("![视频](http://%s/video/%s)", m.Contents["host"], strings.Join(keylist, ","))
	case 47:
		return "[动画表情]"
	case 49:
		switch m.SubType {
		case 5:
			return fmt.Sprintf("[链接|%s](%s)", m.Contents["title"], m.Contents["url"])
		case 6:
			return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Contents["title"], m.Contents["host"], m.Contents["md5"])
		case 8:
			return "[GIF表情]"
		case 19:
			_recordInfo, ok := m.Contents["recordInfo"]
			if !ok {
				return "[合并转发]"
			}
			recordInfo, ok := _recordInfo.(*RecordInfo)
			if !ok {
				return "[合并转发]"
			}
			host := ""
			if m.Contents["host"] != nil {
				host = m.Contents["host"].(string)
			}
			return recordInfo.String("", host)
		case 33, 36:
			if m.Contents["title"] == "" {
				return "[小程序]"
			}
			return fmt.Sprintf("[小程序|%s](%s)", m.Contents["title"], m.Contents["url"])
		case 51:
			if m.Contents["title"] == "" {
				return "[视频号]"
			} else {
				return fmt.Sprintf("[视频号|%s](%s)", m.Contents["title"], m.Contents["url"])
			}
		case 57:
			_refer, ok := m.Contents["refer"]
			if !ok {
				if m.Content == "" {
					return "[引用]"
				}
				return "> [引用]\n" + m.Content
			}
			refer, ok := _refer.(*Message)
			if !ok {
				if m.Content == "" {
					return "[引用]"
				}
				return "> [引用]\n" + m.Content
			}
			buf := strings.Builder{}
			host := ""
			if m.Contents["host"] != nil {
				host = m.Contents["host"].(string)
			}
			referContent := refer.PlainText(false, "", host)
			for _, line := range strings.Split(referContent, "\n") {
				if line == "" {
					continue
				}
				buf.WriteString("> ")
				buf.WriteString(line)
				buf.WriteString("\n")
			}
			buf.WriteString(m.Content)
			return buf.String()
		case 62:
			return m.Content
		case 63:
			return "[视频号]"
		case 87:
			return "[群公告]"
		case 2000:
			return m.Content
		case 2001:
			return "[红包]"
		case 2003:
			return "[红包封面]"
		default:
			return "[分享]"
		}
	case 50:
		return "[语音通话]"
	case 10000:
		return m.Content
	default:
		content := m.Content
		if len(content) > 120 {
			content = content[:120] + "<...>"
		}
		return fmt.Sprintf("Type: %d Content: %s", m.Type, content)
	}
}

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.36.5
// 	protoc        v5.29.3
// source: bytesextra.proto

package wxproto

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type BytesExtraHeader struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Field1        int32                  `protobuf:"varint,1,opt,name=field1,proto3" json:"field1,omitempty"`
	Field2        int32                  `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *BytesExtraHeader) Reset() {
	*x = BytesExtraHeader{}
	mi := &file_bytesextra_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *BytesExtraHeader) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*BytesExtraHeader) ProtoMessage() {}

func (x *BytesExtraHeader) ProtoReflect() protoreflect.Message {
	mi := &file_bytesextra_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use BytesExtraHeader.ProtoReflect.Descriptor instead.
func (*BytesExtraHeader) Descriptor() ([]byte, []int) {
	return file_bytesextra_proto_rawDescGZIP(), []int{0}
}

func (x *BytesExtraHeader) GetField1() int32 {
	if x != nil {
		return x.Field1
	}
	return 0
}

func (x *BytesExtraHeader) GetField2() int32 {
	if x != nil {
		return x.Field2
	}
	return 0
}

type BytesExtraItem struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Type          int32                  `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"`
	Value         string                 `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *BytesExtraItem) Reset() {
	*x = BytesExtraItem{}
	mi := &file_bytesextra_proto_msgTypes[1]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *BytesExtraItem) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*BytesExtraItem) ProtoMessage() {}

func (x *BytesExtraItem) ProtoReflect() protoreflect.Message {
	mi := &file_bytesextra_proto_msgTypes[1]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use BytesExtraItem.ProtoReflect.Descriptor instead.
func (*BytesExtraItem) Descriptor() ([]byte, []int) {
	return file_bytesextra_proto_rawDescGZIP(), []int{1}
}

func (x *BytesExtraItem) GetType() int32 {
	if x != nil {
		return x.Type
	}
	return 0
}

func (x *BytesExtraItem) GetValue() string {
	if x != nil {
		return x.Value
	}
	return ""
}

type BytesExtra struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Header        *BytesExtraHeader      `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
	Items         []*BytesExtraItem      `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *BytesExtra) Reset() {
	*x = BytesExtra{}
	mi := &file_bytesextra_proto_msgTypes[2]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *BytesExtra) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*BytesExtra) ProtoMessage() {}

func (x *BytesExtra) ProtoReflect() protoreflect.Message {
	mi := &file_bytesextra_proto_msgTypes[2]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use BytesExtra.ProtoReflect.Descriptor instead.
func (*BytesExtra) Descriptor() ([]byte, []int) {
	return file_bytesextra_proto_rawDescGZIP(), []int{2}
}

func (x *BytesExtra) GetHeader() *BytesExtraHeader {
	if x != nil {
		return x.Header
	}
	return nil
}

func (x *BytesExtra) GetItems() []*BytesExtraItem {
	if x != nil {
		return x.Items
	}
	return nil
}

var File_bytesextra_proto protoreflect.FileDescriptor

var file_bytesextra_proto_rawDesc = string([]byte{
	0x0a, 0x10, 0x62, 0x79, 0x74, 0x65, 0x73, 0x65, 0x78, 0x74, 0x72, 0x61, 0x2e, 0x70, 0x72, 0x6f,
	0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
	0x22, 0x42, 0x0a, 0x10, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65,
	0x61, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x18, 0x01,
	0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x12, 0x16, 0x0a, 0x06,
	0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69,
	0x65, 0x6c, 0x64, 0x32, 0x22, 0x3a, 0x0a, 0x0e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74,
	0x72, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
	0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
	0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
	0x22, 0x78, 0x0a, 0x0a, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x12, 0x36,
	0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e,
	0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79,
	0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06,
	0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18,
	0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
	0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49,
	0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b,
	0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})

var (
	file_bytesextra_proto_rawDescOnce sync.Once
	file_bytesextra_proto_rawDescData []byte
)

func file_bytesextra_proto_rawDescGZIP() []byte {
	file_bytesextra_proto_rawDescOnce.Do(func() {
		file_bytesextra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)))
	})
	return file_bytesextra_proto_rawDescData
}

var file_bytesextra_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_bytesextra_proto_goTypes = []any{
	(*BytesExtraHeader)(nil), // 0: app.protobuf.BytesExtraHeader
	(*BytesExtraItem)(nil),   // 1: app.protobuf.BytesExtraItem
	(*BytesExtra)(nil),       // 2: app.protobuf.BytesExtra
}
var file_bytesextra_proto_depIdxs = []int32{
	0, // 0: app.protobuf.BytesExtra.header:type_name -> app.protobuf.BytesExtraHeader
	1, // 1: app.protobuf.BytesExtra.items:type_name -> app.protobuf.BytesExtraItem
	2, // [2:2] is the sub-list for method output_type
	2, // [2:2] is the sub-list for method input_type
	2, // [2:2] is the sub-list for extension type_name
	2, // [2:2] is the sub-list for extension extendee
	0, // [0:2] is the sub-list for field type_name
}

func init() { file_bytesextra_proto_init() }
func file_bytesextra_proto_init() {
	if File_bytesextra_proto != nil {
		return
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)),
			NumEnums:      0,
			NumMessages:   3,
			NumExtensions: 0,
			NumServices:   0,
		},
		GoTypes:           file_bytesextra_proto_goTypes,
		DependencyIndexes: file_bytesextra_proto_depIdxs,
		MessageInfos:      file_bytesextra_proto_msgTypes,
	}.Build()
	File_bytesextra_proto = out.File
	file_bytesextra_proto_goTypes = nil
	file_bytesextra_proto_depIdxs = nil
}

package http

import (
	"context"
	"net/http"
	"time"

	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/chatlog/database"
	"github.com/sjzar/chatlog/internal/chatlog/mcp"
	"github.com/sjzar/chatlog/internal/errors"

	"github.com/gin-gonic/gin"
	"github.com/rs/zerolog/log"
)

const (
	DefalutHTTPAddr = "127.0.0.1:5030"
)

type Service struct {
	ctx *ctx.Context
	db  *database.Service
	mcp *mcp.Service

	router *gin.Engine
	server *http.Server
}

func NewService(ctx *ctx.Context, db *database.Service, mcp *mcp.Service) *Service {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()

	// Handle error from SetTrustedProxies
	if err := router.SetTrustedProxies(nil); err != nil {
		log.Err(err).Msg("Failed to set trusted proxies")
	}

	// Middleware
	router.Use(
		errors.RecoveryMiddleware(),
		errors.ErrorHandlerMiddleware(),
		gin.LoggerWithWriter(log.Logger),
	)

	s := &Service{
		ctx:    ctx,
		db:     db,
		mcp:    mcp,
		router: router,
	}

	s.initRouter()
	return s
}

func (s *Service) Start() error {

	if s.ctx.HTTPAddr == "" {
		s.ctx.HTTPAddr = DefalutHTTPAddr
	}

	s.server = &http.Server{
		Addr:    s.ctx.HTTPAddr,
		Handler: s.router,
	}

	go func() {
		// Handle error from Run
		if err := s.server.ListenAndServe(); err != nil {
			log.Err(err).Msg("Failed to start HTTP server")
		}
	}()

	log.Info().Msg("Starting HTTP server on " + s.ctx.HTTPAddr)

	return nil
}

func (s *Service) ListenAndServe() error {

	if s.ctx.HTTPAddr == "" {
		s.ctx.HTTPAddr = DefalutHTTPAddr
	}

	s.server = &http.Server{
		Addr:    s.ctx.HTTPAddr,
		Handler: s.router,
	}

	log.Info().Msg("Starting HTTP server on " + s.ctx.HTTPAddr)
	return s.server.ListenAndServe()
}

func (s *Service) Stop() error {

	if s.server == nil {
		return nil
	}

	// 使用超时上下文优雅关闭
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	if err := s.server.Shutdown(ctx); err != nil {
		log.Debug().Err(err).Msg("Failed to shutdown HTTP server")
		return nil
	}

	log.Info().Msg("HTTP server stopped")
	return nil
}

func (s *Service) GetRouter() *gin.Engine {
	return s.router
}

package chatlog

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/sjzar/chatlog/internal/chatlog/conf"
	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/chatlog/database"
	"github.com/sjzar/chatlog/internal/chatlog/http"
	"github.com/sjzar/chatlog/internal/chatlog/mcp"
	"github.com/sjzar/chatlog/internal/chatlog/wechat"
	iwechat "github.com/sjzar/chatlog/internal/wechat"
	"github.com/sjzar/chatlog/pkg/util"
	"github.com/sjzar/chatlog/pkg/util/dat2img"
)

// Manager 管理聊天日志应用
type Manager struct {
	conf *conf.Service
	ctx  *ctx.Context

	// Services
	db     *database.Service
	http   *http.Service
	mcp    *mcp.Service
	wechat *wechat.Service

	// Terminal UI
	app *App
}

func New(configPath string) (*Manager, error) {

	// 创建配置服务
	conf, err := conf.NewService(configPath)
	if err != nil {
		return nil, err
	}

	// 创建应用上下文
	ctx := ctx.New(conf)

	wechat := wechat.NewService(ctx)

	db := database.NewService(ctx)

	mcp := mcp.NewService(ctx, db)

	http := http.NewService(ctx, db, mcp)

	return &Manager{
		conf:   conf,
		ctx:    ctx,
		db:     db,
		mcp:    mcp,
		http:   http,
		wechat: wechat,
	}, nil
}

func (m *Manager) Run() error {

	m.ctx.WeChatInstances = m.wechat.GetWeChatInstances()
	if len(m.ctx.WeChatInstances) >= 1 {
		m.ctx.SwitchCurrent(m.ctx.WeChatInstances[0])
	}

	if m.ctx.HTTPEnabled {
		// 启动HTTP服务
		if err := m.StartService(); err != nil {
			m.StopService()
		}
	}
	// 启动终端UI
	m.app = NewApp(m.ctx, m)
	m.app.Run() // 阻塞
	return nil
}

func (m *Manager) Switch(info *iwechat.Account, history string) error {
	if m.ctx.AutoDecrypt {
		if err := m.StopAutoDecrypt(); err != nil {
			return err
		}
	}
	if m.ctx.HTTPEnabled {
		if err := m.stopService(); err != nil {
			return err
		}
	}
	if info != nil {
		m.ctx.SwitchCurrent(info)
	} else {
		m.ctx.SwitchHistory(history)
	}

	if m.ctx.HTTPEnabled {
		// 启动HTTP服务
		if err := m.StartService(); err != nil {
			log.Info().Err(err).Msg("启动服务失败")
			m.StopService()
		}
	}
	return nil
}

func (m *Manager) StartService() error {

	// 按依赖顺序启动服务
	if err := m.db.Start(); err != nil {
		return err
	}

	if err := m.mcp.Start(); err != nil {
		m.db.Stop() // 回滚已启动的服务
		return err
	}

	if err := m.http.Start(); err != nil {
		m.mcp.Stop() // 回滚已启动的服务
		m.db.Stop()
		return err
	}

	// 如果是 4.0 版本，更新下 xorkey
	if m.ctx.Version == 4 {
		go dat2img.ScanAndSetXorKey(m.ctx.DataDir)
	}

	// 更新状态
	m.ctx.SetHTTPEnabled(true)

	return nil
}

func (m *Manager) StopService() error {
	if err := m.stopService(); err != nil {
		return err
	}

	// 更新状态
	m.ctx.SetHTTPEnabled(false)

	return nil
}

func (m *Manager) stopService() error {
	// 按依赖的反序停止服务
	var errs []error

	if err := m.http.Stop(); err != nil {
		errs = append(errs, err)
	}

	if err := m.mcp.Stop(); err != nil {
		errs = append(errs, err)
	}

	if err := m.db.Stop(); err != nil {
		errs = append(errs, err)
	}

	// 如果有错误，返回第一个错误
	if len(errs) > 0 {
		return errs[0]
	}

	return nil
}

func (m *Manager) SetHTTPAddr(text string) error {
	var addr string
	if util.IsNumeric(text) {
		addr = fmt.Sprintf("127.0.0.1:%s", text)
	} else if strings.HasPrefix(text, "http://") {
		addr = strings.TrimPrefix(text, "http://")
	} else if strings.HasPrefix(text, "https://") {
		addr = strings.TrimPrefix(text, "https://")
	} else {
		addr = text
	}
	m.ctx.SetHTTPAddr(addr)
	return nil
}

func (m *Manager) GetDataKey() error {
	if m.ctx.Current == nil {
		return fmt.Errorf("未选择任何账号")
	}
	if _, err := m.wechat.GetDataKey(m.ctx.Current); err != nil {
		return err
	}
	m.ctx.Refresh()
	m.ctx.UpdateConfig()
	return nil
}

func (m *Manager) DecryptDBFiles() error {
	if m.ctx.DataKey == "" {
		if m.ctx.Current == nil {
			return fmt.Errorf("未选择任何账号")
		}
		if err := m.GetDataKey(); err != nil {
			return err
		}
	}
	if m.ctx.WorkDir == "" {
		m.ctx.WorkDir = util.DefaultWorkDir(m.ctx.Account)
	}

	if err := m.wechat.DecryptDBFiles(); err != nil {
		return err
	}
	m.ctx.Refresh()
	m.ctx.UpdateConfig()
	return nil
}

func (m *Manager) StartAutoDecrypt() error {
	if m.ctx.DataKey == "" || m.ctx.DataDir == "" {
		return fmt.Errorf("请先获取密钥")
	}
	if m.ctx.WorkDir == "" {
		return fmt.Errorf("请先执行解密数据")
	}

	if err := m.wechat.StartAutoDecrypt(); err != nil {
		return err
	}

	m.ctx.SetAutoDecrypt(true)
	return nil
}

func (m *Manager) StopAutoDecrypt() error {
	if err := m.wechat.StopAutoDecrypt(); err != nil {
		return err
	}

	m.ctx.SetAutoDecrypt(false)
	return nil
}

func (m *Manager) RefreshSession() error {
	if m.db.GetDB() == nil {
		if err := m.db.Start(); err != nil {
			return err
		}
	}
	resp, err := m.db.GetSessions("", 1, 0)
	if err != nil {
		return err
	}
	if len(resp.Items) == 0 {
		return nil
	}
	m.ctx.LastSession = resp.Items[0].NTime
	return nil
}

func (m *Manager) CommandKey(pid int) (string, error) {
	instances := m.wechat.GetWeChatInstances()
	if len(instances) == 0 {
		return "", fmt.Errorf("wechat process not found")
	}
	if len(instances) == 1 {
		return instances[0].GetKey(context.Background())
	}
	if pid == 0 {
		str := "Select a process:\n"
		for _, ins := range instances {
			str += fmt.Sprintf("PID: %d. %s[Version: %s Data Dir: %s ]\n", ins.PID, ins.Name, ins.FullVersion, ins.DataDir)
		}
		return str, nil
	}
	for _, ins := range instances {
		if ins.PID == uint32(pid) {
			return ins.GetKey(context.Background())
		}
	}
	return "", fmt.Errorf("wechat process not found")
}

func (m *Manager) CommandDecrypt(dataDir string, workDir string, key string, platform string, version int) error {
	if dataDir == "" {
		return fmt.Errorf("dataDir is required")
	}
	if key == "" {
		return fmt.Errorf("key is required")
	}
	if workDir == "" {
		workDir = util.DefaultWorkDir(filepath.Base(filepath.Dir(dataDir)))
	}
	m.ctx.DataDir = dataDir
	m.ctx.WorkDir = workDir
	m.ctx.DataKey = key
	m.ctx.Platform = platform
	m.ctx.Version = version
	if err := m.wechat.DecryptDBFiles(); err != nil {
		return err
	}

	return nil
}

func (m *Manager) CommandHTTPServer(addr string, dataDir string, workDir string, platform string, version int) error {

	if addr == "" {
		addr = "127.0.0.1:5030"
	}

	if workDir == "" {
		return fmt.Errorf("workDir is required")
	}

	if platform == "" {
		return fmt.Errorf("platform is required")
	}

	if version == 0 {
		return fmt.Errorf("version is required")
	}

	m.ctx.HTTPAddr = addr
	m.ctx.DataDir = dataDir
	m.ctx.WorkDir = workDir
	m.ctx.Platform = platform
	m.ctx.Version = version

	// 如果是 4.0 版本，更新下 xorkey
	if m.ctx.Version == 4 && m.ctx.DataDir != "" {
		go dat2img.ScanAndSetXorKey(m.ctx.DataDir)
	}

	// 按依赖顺序启动服务
	if err := m.db.Start(); err != nil {
		return err
	}

	if err := m.mcp.Start(); err != nil {
		return err
	}

	return m.http.ListenAndServe()
}

package mcp

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/chatlog/database"
	"github.com/sjzar/chatlog/internal/mcp"
	"github.com/sjzar/chatlog/pkg/util"

	"github.com/gin-gonic/gin"
)

type Service struct {
	ctx *ctx.Context
	db  *database.Service

	mcp *mcp.MCP
}

func NewService(ctx *ctx.Context, db *database.Service) *Service {
	return &Service{
		ctx: ctx,
		db:  db,
	}
}

// GetMCP 获取底层MCP实例
func (s *Service) GetMCP() *mcp.MCP {
	return s.mcp
}

// Start 启动MCP服务
func (s *Service) Start() error {
	s.mcp = mcp.NewMCP()
	go s.worker()
	return nil
}

// Stop 停止MCP服务
func (s *Service) Stop() error {
	if s.mcp != nil {
		s.mcp.Close()
	}
	return nil
}

// worker 处理MCP请求
func (s *Service) worker() {
	for {
		select {
		case p, ok := <-s.mcp.ProcessChan:
			if !ok {
				return
			}
			s.processMCP(p.Session, p.Request)
		}
	}
}

func (s *Service) HandleSSE(c *gin.Context) {
	s.mcp.HandleSSE(c)
}

func (s *Service) HandleMessages(c *gin.Context) {
	s.mcp.HandleMessages(c)
}

// processMCP 处理MCP请求
func (s *Service) processMCP(session *mcp.Session, req *mcp.Request) {
	var err error
	switch req.Method {
	case mcp.MethodInitialize:
		err = s.initialize(session, req)
	case mcp.MethodToolsList:
		err = s.sendCustomParams(session, req, mcp.M{"tools": []mcp.Tool{
			ToolContact,
			ToolChatRoom,
			ToolRecentChat,
			ToolChatLog,
			ToolCurrentTime,
		}})
	case mcp.MethodToolsCall:
		err = s.toolsCall(session, req)
	case mcp.MethodPromptsList:
		err = s.sendCustomParams(session, req, mcp.M{"prompts": []mcp.Prompt{}})
	case mcp.MethodResourcesList:
		err = s.sendCustomParams(session, req, mcp.M{"resources": []mcp.Resource{
			ResourceRecentChat,
		}})
	case mcp.MethodResourcesTemplateList:
		err = s.sendCustomParams(session, req, mcp.M{"resourceTemplates": []mcp.ResourceTemplate{
			ResourceTemplateContact,
			ResourceTemplateChatRoom,
			ResourceTemplateChatlog,
		}})
	case mcp.MethodResourcesRead:
		err = s.resourcesRead(session, req)
	case mcp.MethodPing:
		err = s.sendCustomParams(session, req, struct{}{})
	}

	if err != nil {
		session.WriteError(req, err)
	}
}

// initialize 处理初始化请求
func (s *Service) initialize(session *mcp.Session, req *mcp.Request) error {
	initReq, err := parseParams[mcp.InitializeRequest](req.Params)
	if err != nil {
		return fmt.Errorf("解析初始化参数失败: %v", err)
	}
	session.SaveClientInfo(initReq.ClientInfo)

	return session.WriteResponse(req, InitializeResponse)
}

// toolsCall 处理工具调用
func (s *Service) toolsCall(session *mcp.Session, req *mcp.Request) error {
	callReq, err := parseParams[mcp.ToolsCallRequest](req.Params)
	if err != nil {
		return fmt.Errorf("解析工具调用参数失败: %v", err)
	}

	buf := &bytes.Buffer{}
	switch callReq.Name {
	case "query_contact":
		keyword := ""
		if v, ok := callReq.Arguments["keyword"]; ok {
			keyword = v.(string)
		}
		limit := util.MustAnyToInt(callReq.Arguments["limit"])
		offset := util.MustAnyToInt(callReq.Arguments["offset"])
		list, err := s.db.GetContacts(keyword, limit, offset)
		if err != nil {
			return fmt.Errorf("无法获取联系人列表: %v", err)
		}
		buf.WriteString("UserName,Alias,Remark,NickName\n")
		for _, contact := range list.Items {
			buf.WriteString(fmt.Sprintf("%s,%s,%s,%s\n", contact.UserName, contact.Alias, contact.Remark, contact.NickName))
		}
	case "query_chat_room":
		keyword := ""
		if v, ok := callReq.Arguments["keyword"]; ok {
			keyword = v.(string)
		}
		limit := util.MustAnyToInt(callReq.Arguments["limit"])
		offset := util.MustAnyToInt(callReq.Arguments["offset"])
		list, err := s.db.GetChatRooms(keyword, limit, offset)
		if err != nil {
			return fmt.Errorf("无法获取群聊列表: %v", err)
		}
		buf.WriteString("Name,Remark,NickName,Owner,UserCount\n")
		for _, chatRoom := range list.Items {
			buf.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d\n", chatRoom.Name, chatRoom.Remark, chatRoom.NickName, chatRoom.Owner, len(chatRoom.Users)))
		}
	case "query_recent_chat":
		keyword := ""
		if v, ok := callReq.Arguments["keyword"]; ok {
			keyword = v.(string)
		}
		limit := util.MustAnyToInt(callReq.Arguments["limit"])
		offset := util.MustAnyToInt(callReq.Arguments["offset"])
		data, err := s.db.GetSessions(keyword, limit, offset)
		if err != nil {
			return fmt.Errorf("无法获取会话列表: %v", err)
		}
		for _, session := range data.Items {
			buf.WriteString(session.PlainText(120))
			buf.WriteString("\n")
		}
	case "chatlog":
		if callReq.Arguments == nil {
			return mcp.ErrInvalidParams
		}
		_time := ""
		if v, ok := callReq.Arguments["time"]; ok {
			_time = v.(string)
		}
		start, end, ok := util.TimeRangeOf(_time)
		if !ok {
			return fmt.Errorf("无法解析时间范围")
		}
		talker := ""
		if v, ok := callReq.Arguments["talker"]; ok {
			talker = v.(string)
		}
		sender := ""
		if v, ok := callReq.Arguments["sender"]; ok {
			sender = v.(string)
		}
		keyword := ""
		if v, ok := callReq.Arguments["keyword"]; ok {
			keyword = v.(string)
		}
		limit := util.MustAnyToInt(callReq.Arguments["limit"])
		offset := util.MustAnyToInt(callReq.Arguments["offset"])
		messages, err := s.db.GetMessages(start, end, talker, sender, keyword, limit, offset)
		if err != nil {
			return fmt.Errorf("无法获取聊天记录: %v", err)
		}
		if len(messages) == 0 {
			buf.WriteString("未找到符合查询条件的聊天记录")
		}
		for _, m := range messages {
			buf.WriteString(m.PlainText(strings.Contains(talker, ","), util.PerfectTimeFormat(start, end), ""))
			buf.WriteString("\n")
		}
	case "current_time":
		buf.WriteString(time.Now().Local().Format(time.RFC3339))
	default:
		return fmt.Errorf("未支持的工具: %s", callReq.Name)
	}

	resp := mcp.ToolsCallResponse{
		Content: []mcp.Content{
			{Type: "text", Text: buf.String()},
		},
		IsError: false,
	}
	return session.WriteResponse(req, resp)
}

// resourcesRead 处理资源读取
func (s *Service) resourcesRead(session *mcp.Session, req *mcp.Request) error {
	readReq, err := parseParams[mcp.ResourcesReadRequest](req.Params)
	if err != nil {
		return fmt.Errorf("解析资源读取参数失败: %v", err)
	}

	u, err := url.Parse(readReq.URI)
	if err != nil {
		return fmt.Errorf("无法解析URI: %v", err)
	}

	buf := &bytes.Buffer{}
	switch u.Scheme {
	case "contact":
		list, err := s.db.GetContacts(u.Host, 0, 0)
		if err != nil {
			return fmt.Errorf("无法获取联系人列表: %v", err)
		}
		buf.WriteString("UserName,Alias,Remark,NickName\n")
		for _, contact := range list.Items {
			buf.WriteString(fmt.Sprintf("%s,%s,%s,%s\n", contact.UserName, contact.Alias, contact.Remark, contact.NickName))
		}
	case "chatroom":
		list, err := s.db.GetChatRooms(u.Host, 0, 0)
		if err != nil {
			return fmt.Errorf("无法获取群聊列表: %v", err)
		}
		buf.WriteString("Name,Remark,NickName,Owner,UserCount\n")
		for _, chatRoom := range list.Items {
			buf.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d\n", chatRoom.Name, chatRoom.Remark, chatRoom.NickName, chatRoom.Owner, len(chatRoom.Users)))
		}
	case "session":
		data, err := s.db.GetSessions("", 0, 0)
		if err != nil {
			return fmt.Errorf("无法获取会话列表: %v", err)
		}
		for _, session := range data.Items {
			buf.WriteString(session.PlainText(120))
			buf.WriteString("\n")
		}
	case "chatlog":
		start, end, ok := util.TimeRangeOf(strings.TrimPrefix(u.Path, "/"))
		if !ok {
			return fmt.Errorf("无法解析时间范围")
		}
		limit := util.MustAnyToInt(u.Query().Get("limit"))
		offset := util.MustAnyToInt(u.Query().Get("offset"))
		messages, err := s.db.GetMessages(start, end, u.Host, "", "", limit, offset)
		if err != nil {
			return fmt.Errorf("无法获取聊天记录: %v", err)
		}
		if len(messages) == 0 {
			buf.WriteString("未找到符合查询条件的聊天记录")
		}
		for _, m := range messages {
			buf.WriteString(m.PlainText(strings.Contains(u.Host, ","), util.PerfectTimeFormat(start, end), ""))
			buf.WriteString("\n")
		}
	default:
		return fmt.Errorf("不支持的URI: %s", readReq.URI)
	}

	resp := mcp.ReadingResource{
		Contents: []mcp.ReadingResourceContent{
			{URI: readReq.URI, Text: buf.String()},
		},
	}
	return session.WriteResponse(req, resp)
}

// sendCustomParams 发送自定义参数
func (s *Service) sendCustomParams(session *mcp.Session, req *mcp.Request, params interface{}) error {
	b, err := json.Marshal(mcp.NewResponse(req.ID, params))
	if err != nil {
		return fmt.Errorf("无法序列化响应: %v", err)
	}
	session.Write(b)
	return nil
}

// parseParams 解析参数
func parseParams[T any](params interface{}) (*T, error) {
	if params == nil {
		return nil, errors.New("params is nil")
	}

	// 将 params 重新编码为 JSON
	jsonData, err := json.Marshal(params)
	if err != nil {
		return nil, fmt.Errorf("无法编码 params: %v", err)
	}

	// 解码到目标结构体
	var result T
	if err := json.Unmarshal(jsonData, &result); err != nil {
		return nil, fmt.Errorf("无法解码为目标结构体: %v", err)
	}

	return &result, nil
}

package model

import (
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"github.com/sjzar/chatlog/internal/model/wxproto"
	"github.com/sjzar/chatlog/pkg/util/lz4"
	"google.golang.org/protobuf/proto"
)

// CREATE TABLE MSG (
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
// TalkerId INT DEFAULT 0,
// MsgSvrID INT,
// Type INT,
// SubType INT,
// IsSender INT,
// CreateTime INT,
// Sequence INT DEFAULT 0,
// StatusEx INT DEFAULT 0,
// FlagEx INT,
// Status INT,
// MsgServerSeq INT,
// MsgSequence INT,
// StrTalker TEXT,
// StrContent TEXT,
// DisplayContent TEXT,
// Reserved0 INT DEFAULT 0,
// Reserved1 INT DEFAULT 0,
// Reserved2 INT DEFAULT 0,
// Reserved3 INT DEFAULT 0,
// Reserved4 TEXT,
// Reserved5 TEXT,
// Reserved6 TEXT,
// CompressContent BLOB,
// BytesExtra BLOB,
// BytesTrans BLOB
// )
type MessageV3 struct {
	MsgSvrID        int64  `json:"MsgSvrID"`        // 消息 ID
	Sequence        int64  `json:"Sequence"`        // 消息序号，10位时间戳 + 3位序号
	CreateTime      int64  `json:"CreateTime"`      // 消息创建时间，10位时间戳
	StrTalker       string `json:"StrTalker"`       // 聊天对象，微信 ID or 群 ID
	IsSender        int    `json:"IsSender"`        // 是否为发送消息，0 接收消息，1 发送消息
	Type            int64  `json:"Type"`            // 消息类型
	SubType         int    `json:"SubType"`         // 消息子类型
	StrContent      string `json:"StrContent"`      // 消息内容，文字聊天内容 或 XML
	CompressContent []byte `json:"CompressContent"` // 非文字聊天内容，如图片、语音、视频等
	BytesExtra      []byte `json:"BytesExtra"`      // protobuf 额外数据，记录群聊发送人等信息
}

func (m *MessageV3) Wrap() *Message {

	_m := &Message{
		Seq:        m.Sequence,
		Time:       time.Unix(m.CreateTime, 0),
		Talker:     m.StrTalker,
		IsChatRoom: strings.HasSuffix(m.StrTalker, "@chatroom"),
		IsSelf:     m.IsSender == 1,
		Type:       m.Type,
		SubType:    int64(m.SubType),
		Content:    m.StrContent,
		Version:    WeChatV3,
	}

	if !_m.IsChatRoom && !_m.IsSelf {
		_m.Sender = m.StrTalker
	}

	if _m.Type == 49 {
		b, err := lz4.Decompress(m.CompressContent)
		if err == nil {
			_m.Content = string(b)
		}
	}

	_m.ParseMediaInfo(_m.Content)

	// 语音消息
	if _m.Type == 34 {
		_m.Contents["voice"] = fmt.Sprint(m.MsgSvrID)
	}

	if len(m.BytesExtra) != 0 {
		if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil {
			if _m.IsChatRoom {
				_m.Sender = bytesExtra[1]
			}
			// FIXME xml 中的 md5 数据无法匹配到 hardlink 记录，所以直接用 proto 数据
			if _m.Type == 43 {
				path := bytesExtra[4]
				parts := strings.Split(filepath.ToSlash(path), "/")
				if len(parts) > 1 {
					path = strings.Join(parts[1:], "/")
				}
				_m.Contents["videofile"] = path
			}
		}
	}

	return _m
}

// ParseBytesExtra 解析额外数据
// 按需解析
func ParseBytesExtra(b []byte) map[int]string {
	var pbMsg wxproto.BytesExtra
	if err := proto.Unmarshal(b, &pbMsg); err != nil {
		return nil
	}
	if pbMsg.Items == nil {
		return nil
	}

	ret := make(map[int]string, len(pbMsg.Items))
	for _, item := range pbMsg.Items {
		ret[int(item.Type)] = item.Value
	}

	return ret
}

package http

import (
	"embed"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/pkg/util"
	"github.com/sjzar/chatlog/pkg/util/dat2img"
	"github.com/sjzar/chatlog/pkg/util/silk"

	"github.com/gin-gonic/gin"
)

// EFS holds embedded file system data for static assets.
//
//go:embed static
var EFS embed.FS

func (s *Service) initRouter() {

	router := s.GetRouter()

	staticDir, _ := fs.Sub(EFS, "static")
	router.StaticFS("/static", http.FS(staticDir))
	router.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticDir))
	router.StaticFileFS("/", "./index.htm", http.FS(staticDir))

	// Media
	router.GET("/image/*key", s.GetImage)
	router.GET("/video/*key", s.GetVideo)
	router.GET("/file/*key", s.GetFile)
	router.GET("/voice/*key", s.GetVoice)
	router.GET("/data/*path", s.GetMediaData)

	// MCP Server
	{
		router.GET("/sse", s.mcp.HandleSSE)
		router.POST("/messages", s.mcp.HandleMessages)
		// mcp inspector is shit
		// https://github.com/modelcontextprotocol/inspector/blob/aeaf32f/server/src/index.ts#L155
		router.POST("/message", s.mcp.HandleMessages)
	}

	// API V1 Router
	api := router.Group("/api/v1")
	{
		api.GET("/chatlog", s.GetChatlog)
		api.GET("/contact", s.GetContacts)
		api.GET("/chatroom", s.GetChatRooms)
		api.GET("/session", s.GetSessions)
	}

	router.NoRoute(s.NoRoute)
}

// NoRoute handles 404 Not Found errors. If the request URL starts with "/api"
// or "/static", it responds with a JSON error. Otherwise, it redirects to the root path.
func (s *Service) NoRoute(c *gin.Context) {
	path := c.Request.URL.Path
	switch {
	case strings.HasPrefix(path, "/api"), strings.HasPrefix(path, "/static"):
		c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
	default:
		c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
		c.Redirect(http.StatusFound, "/")
	}
}

func (s *Service) GetChatlog(c *gin.Context) {

	q := struct {
		Time    string `form:"time"`
		Talker  string `form:"talker"`
		Sender  string `form:"sender"`
		Keyword string `form:"keyword"`
		Limit   int    `form:"limit"`
		Offset  int    `form:"offset"`
		Format  string `form:"format"`
	}{}

	if err := c.BindQuery(&q); err != nil {
		errors.Err(c, err)
		return
	}

	var err error
	start, end, ok := util.TimeRangeOf(q.Time)
	if !ok {
		errors.Err(c, errors.InvalidArg("time"))
	}
	if q.Limit < 0 {
		q.Limit = 0
	}

	if q.Offset < 0 {
		q.Offset = 0
	}

	messages, err := s.db.GetMessages(start, end, q.Talker, q.Sender, q.Keyword, q.Limit, q.Offset)
	if err != nil {
		errors.Err(c, err)
		return
	}

	switch strings.ToLower(q.Format) {
	case "csv":
	case "json":
		// json
		c.JSON(http.StatusOK, messages)
	default:
		// plain text
		c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
		c.Writer.Header().Set("Cache-Control", "no-cache")
		c.Writer.Header().Set("Connection", "keep-alive")
		c.Writer.Flush()

		for _, m := range messages {
			c.Writer.WriteString(m.PlainText(strings.Contains(q.Talker, ","), util.PerfectTimeFormat(start, end), c.Request.Host))
			c.Writer.WriteString("\n")
			c.Writer.Flush()
		}
	}
}

func (s *Service) GetContacts(c *gin.Context) {

	q := struct {
		Keyword string `form:"keyword"`
		Limit   int    `form:"limit"`
		Offset  int    `form:"offset"`
		Format  string `form:"format"`
	}{}

	if err := c.BindQuery(&q); err != nil {
		errors.Err(c, err)
		return
	}

	list, err := s.db.GetContacts(q.Keyword, q.Limit, q.Offset)
	if err != nil {
		errors.Err(c, err)
		return
	}

	format := strings.ToLower(q.Format)
	switch format {
	case "json":
		// json
		c.JSON(http.StatusOK, list)
	default:
		// csv
		if format == "csv" {
			// 浏览器访问时，会下载文件
			c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
		} else {
			c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
		}
		c.Writer.Header().Set("Cache-Control", "no-cache")
		c.Writer.Header().Set("Connection", "keep-alive")
		c.Writer.Flush()

		c.Writer.WriteString("UserName,Alias,Remark,NickName\n")
		for _, contact := range list.Items {
			c.Writer.WriteString(fmt.Sprintf("%s,%s,%s,%s\n", contact.UserName, contact.Alias, contact.Remark, contact.NickName))
		}
		c.Writer.Flush()
	}
}

func (s *Service) GetChatRooms(c *gin.Context) {

	q := struct {
		Keyword string `form:"keyword"`
		Limit   int    `form:"limit"`
		Offset  int    `form:"offset"`
		Format  string `form:"format"`
	}{}

	if err := c.BindQuery(&q); err != nil {
		errors.Err(c, err)
		return
	}

	list, err := s.db.GetChatRooms(q.Keyword, q.Limit, q.Offset)
	if err != nil {
		errors.Err(c, err)
		return
	}
	format := strings.ToLower(q.Format)
	switch format {
	case "json":
		// json
		c.JSON(http.StatusOK, list)
	default:
		// csv
		if format == "csv" {
			// 浏览器访问时，会下载文件
			c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
		} else {
			c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
		}
		c.Writer.Header().Set("Cache-Control", "no-cache")
		c.Writer.Header().Set("Connection", "keep-alive")
		c.Writer.Flush()

		c.Writer.WriteString("Name,Remark,NickName,Owner,UserCount\n")
		for _, chatRoom := range list.Items {
			c.Writer.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d\n", chatRoom.Name, chatRoom.Remark, chatRoom.NickName, chatRoom.Owner, len(chatRoom.Users)))
		}
		c.Writer.Flush()
	}
}

func (s *Service) GetSessions(c *gin.Context) {

	q := struct {
		Keyword string `form:"keyword"`
		Limit   int    `form:"limit"`
		Offset  int    `form:"offset"`
		Format  string `form:"format"`
	}{}

	if err := c.BindQuery(&q); err != nil {
		errors.Err(c, err)
		return
	}

	sessions, err := s.db.GetSessions(q.Keyword, q.Limit, q.Offset)
	if err != nil {
		errors.Err(c, err)
		return
	}
	format := strings.ToLower(q.Format)
	switch format {
	case "csv":
		c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
		c.Writer.Header().Set("Cache-Control", "no-cache")
		c.Writer.Header().Set("Connection", "keep-alive")
		c.Writer.Flush()

		c.Writer.WriteString("UserName,NOrder,NickName,Content,NTime\n")
		for _, session := range sessions.Items {
			c.Writer.WriteString(fmt.Sprintf("%s,%d,%s,%s,%s\n", session.UserName, session.NOrder, session.NickName, strings.ReplaceAll(session.Content, "\n", "\\n"), session.NTime))
		}
		c.Writer.Flush()
	case "json":
		// json
		c.JSON(http.StatusOK, sessions)
	default:
		c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
		c.Writer.Header().Set("Cache-Control", "no-cache")
		c.Writer.Header().Set("Connection", "keep-alive")
		c.Writer.Flush()
		for _, session := range sessions.Items {
			c.Writer.WriteString(session.PlainText(120))
			c.Writer.WriteString("\n")
		}
		c.Writer.Flush()
	}
}

func (s *Service) GetImage(c *gin.Context) {
	s.GetMedia(c, "image")
}

func (s *Service) GetVideo(c *gin.Context) {
	s.GetMedia(c, "video")
}

func (s *Service) GetFile(c *gin.Context) {
	s.GetMedia(c, "file")
}
func (s *Service) GetVoice(c *gin.Context) {
	s.GetMedia(c, "voice")
}

func (s *Service) GetMedia(c *gin.Context, _type string) {
	key := strings.TrimPrefix(c.Param("key"), "/")
	if key == "" {
		errors.Err(c, errors.InvalidArg(key))
		return
	}

	keys := util.Str2List(key, ",")
	if len(keys) == 0 {
		errors.Err(c, errors.InvalidArg(key))
		return
	}

	var _err error
	for _, k := range keys {
		if len(k) != 32 {
			absolutePath := filepath.Join(s.ctx.DataDir, k)
			if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
				continue
			}
			c.Redirect(http.StatusFound, "/data/"+k)
			return
		}
		media, err := s.db.GetMedia(_type, k)
		if err != nil {
			_err = err
			continue
		}
		if c.Query("info") != "" {
			c.JSON(http.StatusOK, media)
			return
		}
		switch media.Type {
		case "voice":
			s.HandleVoice(c, media.Data)
			return
		default:
			c.Redirect(http.StatusFound, "/data/"+media.Path)
			return
		}
	}

	if _err != nil {
		errors.Err(c, _err)
		return
	}
}

func (s *Service) GetMediaData(c *gin.Context) {
	relativePath := filepath.Clean(c.Param("path"))

	absolutePath := filepath.Join(s.ctx.DataDir, relativePath)

	if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
		c.JSON(http.StatusNotFound, gin.H{
			"error": "File not found",
		})
		return
	}

	ext := strings.ToLower(filepath.Ext(absolutePath))
	switch {
	case ext == ".dat":
		s.HandleDatFile(c, absolutePath)
	default:
		// 直接返回文件
		c.File(absolutePath)
	}

}

func (s *Service) HandleDatFile(c *gin.Context, path string) {

	b, err := os.ReadFile(path)
	if err != nil {
		errors.Err(c, err)
		return
	}
	out, ext, err := dat2img.Dat2Image(b)
	if err != nil {
		c.File(path)
		return
	}

	switch ext {
	case "jpg":
		c.Data(http.StatusOK, "image/jpeg", out)
	case "png":
		c.Data(http.StatusOK, "image/png", out)
	case "gif":
		c.Data(http.StatusOK, "image/gif", out)
	case "bmp":
		c.Data(http.StatusOK, "image/bmp", out)
	default:
		c.Data(http.StatusOK, "image/jpg", out)
		// c.File(path)
	}
}

func (s *Service) HandleVoice(c *gin.Context, data []byte) {
	out, err := silk.Silk2MP3(data)
	if err != nil {
		c.Data(http.StatusOK, "audio/silk", data)
		return
	}
	c.Data(http.StatusOK, "audio/mp3", out)
}

package form

import (
	"fmt"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
	"github.com/sjzar/chatlog/internal/ui/style"
)

const (
	// DialogPadding dialog inner padding.
	DialogPadding = 3

	// DialogHelpHeight dialog help text height.
	DialogHelpHeight = 1

	// DialogMinWidth dialog min width.
	DialogMinWidth = 40

	// FormHeightOffset form height offset for border.
	FormHeightOffset = 3

	// 额外的宽度补偿，类似于 submenu 的 cmdWidthOffset
	formWidthOffset = 10
)

// Form is a modal form component with a title, form fields, and help text.
type Form struct {
	*tview.Box
	title         string
	layout        *tview.Flex
	form          *tview.Form
	helpText      *tview.TextView
	width         int
	height        int
	cancelHandler func()
	fields        []formField // 存储字段信息以便重新计算宽度
}

// formField 存储表单字段的信息
type formField struct {
	label      string
	value      string
	fieldWidth int
}

// NewForm creates a new form with the given title.
func NewForm(title string) *Form {
	f := &Form{
		Box:    tview.NewBox(),
		title:  title,
		layout: tview.NewFlex().SetDirection(tview.FlexRow),
		form:   tview.NewForm(),
		fields: make([]formField, 0),
	}

	// 设置表单样式
	f.form.SetBorderPadding(1, 1, 1, 1)
	f.form.SetBackgroundColor(style.DialogBgColor)
	f.form.SetFieldBackgroundColor(style.BgColor)
	f.form.SetFieldTextColor(style.FgColor)
	f.form.SetButtonBackgroundColor(style.ButtonBgColor)
	f.form.SetButtonTextColor(style.FgColor)
	f.form.SetLabelColor(style.DialogFgColor)
	f.form.SetButtonsAlign(tview.AlignCenter)

	// 创建帮助文本
	f.helpText = tview.NewTextView()
	f.helpText.SetDynamicColors(true)
	f.helpText.SetTextAlign(tview.AlignCenter)
	f.helpText.SetTextColor(style.DialogFgColor)
	f.helpText.SetBackgroundColor(style.DialogBgColor)
	fmt.Fprintf(f.helpText,
		"[%s::b]Tab[%s::b]: 导航  [%s::b]Enter[%s::b]: 选择  [%s::b]ESC[%s::b]: 返回",
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
	)

	// 创建布局
	formLayout := tview.NewFlex().SetDirection(tview.FlexColumn)
	formLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, false)
	formLayout.AddItem(f.form, 0, 1, true)
	formLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, false)

	// 设置主布局
	f.layout.SetTitle(fmt.Sprintf("[::b]%s", f.title))
	f.layout.SetTitleColor(style.DialogFgColor)
	f.layout.SetTitleAlign(tview.AlignCenter)
	f.layout.SetBorder(true)
	f.layout.SetBorderColor(style.DialogBorderColor)
	f.layout.SetBackgroundColor(style.DialogBgColor)

	// 添加表单区域
	f.layout.AddItem(formLayout, 0, 1, true)

	// 添加帮助文本区域
	f.layout.AddItem(f.helpText, DialogHelpHeight, 0, false)

	return f
}

// AddInputField adds an input field to the form.
func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
	// 存储字段信息
	f.fields = append(f.fields, formField{
		label:      label,
		value:      value,
		fieldWidth: fieldWidth,
	})

	// 添加输入字段到表单
	f.form.AddInputField(label, value, fieldWidth, accept, changed)

	// 更新表单尺寸
	f.recalculateSize()

	return f
}

// AddButton adds a button to the form.
func (f *Form) AddButton(label string, selected func()) *Form {
	f.form.AddButton(label, selected)
	// 更新表单尺寸
	f.recalculateSize()
	return f
}

// AddCheckbox adds a checkbox to the form.
func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
	f.form.AddCheckbox(label, checked, changed)
	// 更新表单尺寸
	f.recalculateSize()
	return f
}

// SetCancelFunc sets the function to be called when the form is cancelled.
func (f *Form) SetCancelFunc(handler func()) *Form {
	f.cancelHandler = handler
	return f
}

// recalculateSize 重新计算表单尺寸
func (f *Form) recalculateSize() {
	// 计算表单项数量
	itemCount := f.form.GetFormItemCount()

	// 计算高度 - 每个表单项占2行，按钮区域至少占2行，再加上边框和帮助文本
	f.height = (itemCount * 2) + 2 + FormHeightOffset + DialogHelpHeight

	// 计算宽度 - 类似于 submenu 的实现
	maxLabelWidth := 0
	maxValueWidth := 0

	// 遍历所有字段，找出最长的标签和值
	for _, field := range f.fields {
		if len(field.label) > maxLabelWidth {
			maxLabelWidth = len(field.label)
		}

		// 对于值，使用字段宽度和实际值长度中的较大者
		valueWidth := field.fieldWidth
		if len(field.value) > valueWidth {
			valueWidth = len(field.value)
		}

		if valueWidth > maxValueWidth {
			maxValueWidth = valueWidth
		}
	}

	// 计算总宽度，类似于 submenu 的计算方式
	f.width = maxLabelWidth + maxValueWidth + formWidthOffset

	// 确保宽度不小于最小值
	if f.width < DialogMinWidth {
		f.width = DialogMinWidth
	}
}

// Draw draws the form on the screen.
func (f *Form) Draw(screen tcell.Screen) {
	// 在绘制前重新计算尺寸，确保尺寸是最新的
	f.recalculateSize()

	// 绘制
	f.Box.DrawForSubclass(screen, f)
	f.layout.Draw(screen)
}

// SetRect sets the position and size of the form.
func (f *Form) SetRect(x, y, width, height int) {
	// 确保尺寸是最新的
	f.recalculateSize()

	// 类似于 submenu 的实现
	ws := (width - f.width) / 2
	hs := (height - f.height) / 2

	// 确保不会超出屏幕
	if f.width > width {
		ws = 0
		f.width = width - 1
	}

	if f.height > height {
		hs = 0
		f.height = height - 1
	}

	// 设置表单位置
	f.Box.SetRect(x+ws, y+hs, f.width, f.height)

	// 获取内部矩形并设置布局
	x, y, width, height = f.Box.GetInnerRect()
	f.layout.SetRect(x, y, width, height)
}

// Focus is called when this primitive receives focus.
func (f *Form) Focus(delegate func(p tview.Primitive)) {
	// 确保表单获得焦点
	if f.form != nil {
		delegate(f.form)
	} else {
		// 如果表单为空，则让Box获得焦点
		delegate(f.Box)
	}
}

// HasFocus returns whether or not this primitive has focus.
func (f *Form) HasFocus() bool {
	return f.form.HasFocus()
}

// InputHandler returns the handler for this primitive.
func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
	return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
		// ESC键处理
		if event.Key() == tcell.KeyEscape && f.cancelHandler != nil {
			f.cancelHandler()
			return
		}

		// 将事件传递给表单
		if handler := f.form.InputHandler(); handler != nil {
			handler(event, setFocus)
		}
	})
}

// EmptyBoxSpace creates an empty box with the specified background color.
func EmptyBoxSpace(bgColor tcell.Color) *tview.Box {
	box := tview.NewBox()
	box.SetBackgroundColor(bgColor)
	box.SetBorder(false)
	return box
}

package repository

import (
	"context"
	"strings"
	"time"

	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/pkg/util"

	"github.com/rs/zerolog/log"
)

// GetMessages 实现 Repository 接口的 GetMessages 方法
func (r *Repository) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {

	talker, sender = r.parseTalkerAndSender(ctx, talker, sender)
	messages, err := r.ds.GetMessages(ctx, startTime, endTime, talker, sender, keyword, limit, offset)
	if err != nil {
		return nil, err
	}

	// 补充消息信息
	if err := r.EnrichMessages(ctx, messages); err != nil {
		log.Debug().Msgf("EnrichMessages failed: %v", err)
	}

	return messages, nil
}

// EnrichMessages 补充消息的额外信息
func (r *Repository) EnrichMessages(ctx context.Context, messages []*model.Message) error {
	for _, msg := range messages {
		r.enrichMessage(msg)
	}
	return nil
}

// enrichMessage 补充单条消息的额外信息
func (r *Repository) enrichMessage(msg *model.Message) {
	// 处理群聊消息
	if msg.IsChatRoom {
		// 补充群聊名称
		if chatRoom, ok := r.chatRoomCache[msg.Talker]; ok {
			msg.TalkerName = chatRoom.DisplayName()

			// 补充发送者在群里的显示名称
			if displayName, ok := chatRoom.User2DisplayName[msg.Sender]; ok {
				msg.SenderName = displayName
			}
		}
	}

	// 如果不是自己发送的消息且还没有显示名称，尝试补充发送者信息
	if msg.SenderName == "" && !msg.IsSelf {
		contact := r.getFullContact(msg.Sender)
		if contact != nil {
			msg.SenderName = contact.DisplayName()
		}
	}
}

func (r *Repository) parseTalkerAndSender(ctx context.Context, talker, sender string) (string, string) {
	displayName2User := make(map[string]string)
	users := make(map[string]bool)

	talkers := util.Str2List(talker, ",")
	if len(talkers) > 0 {
		for i := 0; i < len(talkers); i++ {
			if contact, _ := r.GetContact(ctx, talkers[i]); contact != nil {
				talkers[i] = contact.UserName
			} else if chatRoom, _ := r.GetChatRoom(ctx, talker); chatRoom != nil {
				talkers[i] = chatRoom.Name
			}
		}
		// 获取群聊的用户列表
		for i := 0; i < len(talkers); i++ {
			if chatRoom, _ := r.GetChatRoom(ctx, talkers[i]); chatRoom != nil {
				for user, displayName := range chatRoom.User2DisplayName {
					displayName2User[displayName] = user
				}
				for _, user := range chatRoom.Users {
					users[user.UserName] = true
				}
			}
		}
		talker = strings.Join(talkers, ",")
	}

	senders := util.Str2List(sender, ",")
	if len(senders) > 0 {
		for i := 0; i < len(senders); i++ {
			if user, ok := displayName2User[senders[i]]; ok {
				senders[i] = user
			} else {
				// FIXME 大量群聊用户名称重复，无法直接通过 GetContact 获取 ID，后续再优化
				for user := range users {
					if contact := r.getFullContact(user); contact != nil {
						if contact.DisplayName() == senders[i] {
							senders[i] = user
							break
						}
					}
				}
			}
		}
		sender = strings.Join(senders, ",")
	}

	return talker, sender
}

package ctx

import (
	"sync"
	"time"

	"github.com/sjzar/chatlog/internal/chatlog/conf"
	"github.com/sjzar/chatlog/internal/wechat"
	"github.com/sjzar/chatlog/pkg/util"
)

// Context is a context for a chatlog.
// It is used to store information about the chatlog.
type Context struct {
	conf *conf.Service
	mu   sync.RWMutex

	History map[string]conf.ProcessConfig

	// 微信账号相关状态
	Account     string
	Platform    string
	Version     int
	FullVersion string
	DataDir     string
	DataKey     string
	DataUsage   string

	// 工作目录相关状态
	WorkDir   string
	WorkUsage string

	// HTTP服务相关状态
	HTTPEnabled bool
	HTTPAddr    string

	// 自动解密
	AutoDecrypt bool
	LastSession time.Time

	// 当前选中的微信实例
	Current *wechat.Account
	PID     int
	ExePath string
	Status  string

	// 所有可用的微信实例
	WeChatInstances []*wechat.Account
}

func New(conf *conf.Service) *Context {
	ctx := &Context{
		conf: conf,
	}

	ctx.loadConfig()

	return ctx
}

func (c *Context) loadConfig() {
	conf := c.conf.GetConfig()
	c.History = conf.ParseHistory()
	c.SwitchHistory(conf.LastAccount)
	c.Refresh()
}

func (c *Context) SwitchHistory(account string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.Current = nil
	c.PID = 0
	c.ExePath = ""
	c.Status = ""
	history, ok := c.History[account]
	if ok {
		c.Account = history.Account
		c.Platform = history.Platform
		c.Version = history.Version
		c.FullVersion = history.FullVersion
		c.DataKey = history.DataKey
		c.DataDir = history.DataDir
		c.WorkDir = history.WorkDir
		c.HTTPEnabled = history.HTTPEnabled
		c.HTTPAddr = history.HTTPAddr
	} else {
		c.Account = ""
		c.Platform = ""
		c.Version = 0
		c.FullVersion = ""
		c.DataKey = ""
		c.DataDir = ""
		c.WorkDir = ""
		c.HTTPEnabled = false
		c.HTTPAddr = ""
	}
}

func (c *Context) SwitchCurrent(info *wechat.Account) {
	c.SwitchHistory(info.Name)
	c.mu.Lock()
	defer c.mu.Unlock()
	c.Current = info
	c.Refresh()

}
func (c *Context) Refresh() {
	if c.Current != nil {
		c.Account = c.Current.Name
		c.Platform = c.Current.Platform
		c.Version = c.Current.Version
		c.FullVersion = c.Current.FullVersion
		c.PID = int(c.Current.PID)
		c.ExePath = c.Current.ExePath
		c.Status = c.Current.Status
		if c.Current.Key != "" && c.Current.Key != c.DataKey {
			c.DataKey = c.Current.Key
		}
		if c.Current.DataDir != "" && c.Current.DataDir != c.DataDir {
			c.DataDir = c.Current.DataDir
		}
	}
	if c.DataUsage == "" && c.DataDir != "" {
		go func() {
			c.DataUsage = util.GetDirSize(c.DataDir)
		}()
	}
	if c.WorkUsage == "" && c.WorkDir != "" {
		go func() {
			c.WorkUsage = util.GetDirSize(c.WorkDir)
		}()
	}
}

func (c *Context) SetHTTPEnabled(enabled bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.HTTPEnabled = enabled
	c.UpdateConfig()
}

func (c *Context) SetHTTPAddr(addr string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.HTTPAddr = addr
	c.UpdateConfig()
}

func (c *Context) SetWorkDir(dir string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.WorkDir = dir
	c.UpdateConfig()
	c.Refresh()
}

func (c *Context) SetDataDir(dir string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.DataDir = dir
	c.UpdateConfig()
	c.Refresh()
}

func (c *Context) SetAutoDecrypt(enabled bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.AutoDecrypt = enabled
	c.UpdateConfig()
}

// 更新配置
func (c *Context) UpdateConfig() {
	pconf := conf.ProcessConfig{
		Type:        "wechat",
		Account:     c.Account,
		Platform:    c.Platform,
		Version:     c.Version,
		FullVersion: c.FullVersion,
		DataDir:     c.DataDir,
		DataKey:     c.DataKey,
		WorkDir:     c.WorkDir,
		HTTPEnabled: c.HTTPEnabled,
		HTTPAddr:    c.HTTPAddr,
	}
	conf := c.conf.GetConfig()
	conf.UpdateHistory(c.Account, pconf)
}

package mcp

import (
	"io"
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
)

const (
	ProcessChanCap = 1000
)

type MCP struct {
	sessions  map[string]*Session
	sessionMu sync.Mutex

	ProcessChan chan ProcessCtx
}

func NewMCP() *MCP {
	return &MCP{
		sessions:    make(map[string]*Session),
		ProcessChan: make(chan ProcessCtx, ProcessChanCap),
	}
}

func (m *MCP) HandleSSE(c *gin.Context) {
	id := uuid.New().String()
	m.sessionMu.Lock()
	m.sessions[id] = NewSession(c, id)
	m.sessionMu.Unlock()

	c.Stream(func(w io.Writer) bool {
		<-c.Request.Context().Done()
		return false
	})

	m.sessionMu.Lock()
	delete(m.sessions, id)
	m.sessionMu.Unlock()
}

func (m *MCP) GetSession(id string) *Session {
	m.sessionMu.Lock()
	defer m.sessionMu.Unlock()
	return m.sessions[id]
}

func (m *MCP) HandleMessages(c *gin.Context) {

	// panic("xxx")

	// 啊这, 一个 sessionid 有 3 种写法 session_id, sessionId, sessionid
	// 官方 SDK 是 session_id: https://github.com/modelcontextprotocol/python-sdk/blob/c897868/src/mcp/server/sse.py#L98
	// 写的是 sessionId: https://github.com/modelcontextprotocol/inspector/blob/aeaf32f/server/src/index.ts#L157

	sessionID := c.Query("session_id")
	if sessionID == "" {
		sessionID = c.Query("sessionId")
	}
	if sessionID == "" {
		sessionID = c.Param("sessionid")
	}
	if sessionID == "" {
		c.JSON(http.StatusBadRequest, ErrInvalidSessionID.JsonRPC())
		c.Abort()
		return
	}

	session := m.GetSession(sessionID)
	if session == nil {
		c.JSON(http.StatusNotFound, ErrSessionNotFound.JsonRPC())
		c.Abort()
		return
	}

	var req Request
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrInvalidRequest.JsonRPC())
		c.Abort()
		return
	}

	log.Debug().Msgf("session: %s, request: %s", sessionID, req)
	select {
	case m.ProcessChan <- ProcessCtx{Session: session, Request: &req}:
	default:
		c.JSON(http.StatusTooManyRequests, ErrTooManyRequests.JsonRPC())
		c.Abort()
		return
	}

	c.String(http.StatusAccepted, "Accepted")
}

func (m *MCP) Close() {
	close(m.ProcessChan)
}

type ProcessCtx struct {
	Session *Session
	Request *Request
}

package windows

import (
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/shirou/gopsutil/v4/process"

	"github.com/sjzar/chatlog/internal/wechat/model"
	"github.com/sjzar/chatlog/pkg/appver"
)

const (
	V3ProcessName = "WeChat"
	V4ProcessName = "Weixin"
	V3DBFile      = `Msg\Misc.db`
	V4DBFile      = `db_storage\session\session.db`
)

// Detector 实现 Windows 平台的进程检测器
type Detector struct{}

// NewDetector 创建一个新的 Windows 检测器
func NewDetector() *Detector {
	return &Detector{}
}

// FindProcesses 查找所有微信进程并返回它们的信息
func (d *Detector) FindProcesses() ([]*model.Process, error) {
	processes, err := process.Processes()
	if err != nil {
		log.Err(err).Msg("获取进程列表失败")
		return nil, err
	}

	var result []*model.Process
	for _, p := range processes {
		name, err := p.Name()
		name = strings.TrimSuffix(name, ".exe")
		if err != nil || (name != V3ProcessName && name != V4ProcessName) {
			continue
		}

		// v4 存在同名进程，需要继续判断 cmdline
		if name == V4ProcessName {
			cmdline, err := p.Cmdline()
			if err != nil {
				log.Err(err).Msg("获取进程命令行失败")
				continue
			}
			if strings.Contains(cmdline, "--") {
				continue
			}
		}

		// 获取进程信息
		procInfo, err := d.getProcessInfo(p)
		if err != nil {
			log.Err(err).Msgf("获取进程 %d 的信息失败", p.Pid)
			continue
		}

		result = append(result, procInfo)
	}

	return result, nil
}

// getProcessInfo 获取微信进程的详细信息
func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
	procInfo := &model.Process{
		PID:      uint32(p.Pid),
		Status:   model.StatusOffline,
		Platform: model.PlatformWindows,
	}

	// 获取可执行文件路径
	exePath, err := p.Exe()
	if err != nil {
		log.Err(err).Msg("获取可执行文件路径失败")
		return nil, err
	}
	procInfo.ExePath = exePath

	// 获取版本信息
	versionInfo, err := appver.New(exePath)
	if err != nil {
		log.Err(err).Msg("获取版本信息失败")
		return nil, err
	}
	procInfo.Version = versionInfo.Version
	procInfo.FullVersion = versionInfo.FullVersion

	// 初始化附加信息（数据目录、账户名）
	if err := initializeProcessInfo(p, procInfo); err != nil {
		log.Err(err).Msg("初始化进程信息失败")
		// 即使初始化失败也返回部分信息
	}

	return procInfo, nil
}

package chatlog

import (
	"runtime"

	"github.com/sjzar/chatlog/internal/chatlog"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

func init() {
	rootCmd.AddCommand(serverCmd)
	serverCmd.Flags().StringVarP(&serverAddr, "addr", "a", "127.0.0.1:5030", "server address")
	serverCmd.Flags().StringVarP(&serverDataDir, "data-dir", "d", "", "data dir")
	serverCmd.Flags().StringVarP(&serverWorkDir, "work-dir", "w", "", "work dir")
	serverCmd.Flags().StringVarP(&serverPlatform, "platform", "p", runtime.GOOS, "platform")
	serverCmd.Flags().IntVarP(&serverVer, "version", "v", 3, "version")
}

var (
	serverAddr     string
	serverDataDir  string
	serverWorkDir  string
	serverPlatform string
	serverVer      int
)

var serverCmd = &cobra.Command{
	Use:   "server",
	Short: "Start HTTP server",
	Run: func(cmd *cobra.Command, args []string) {
		m, err := chatlog.New("")
		if err != nil {
			log.Err(err).Msg("failed to create chatlog instance")
			return
		}
		if err := m.CommandHTTPServer(serverAddr, serverDataDir, serverWorkDir, serverPlatform, serverVer); err != nil {
			log.Err(err).Msg("failed to start server")
			return
		}
	},
}

package model

import (
	"github.com/sjzar/chatlog/internal/model/wxproto"

	"google.golang.org/protobuf/proto"
)

type ChatRoom struct {
	Name  string         `json:"name"`
	Owner string         `json:"owner"`
	Users []ChatRoomUser `json:"users"`

	// Extra From Contact
	Remark   string `json:"remark"`
	NickName string `json:"nickName"`

	User2DisplayName map[string]string `json:"-"`
}

type ChatRoomUser struct {
	UserName    string `json:"userName"`
	DisplayName string `json:"displayName"`
}

// CREATE TABLE ChatRoom(
// ChatRoomName TEXT PRIMARY KEY,
// UserNameList TEXT,
// DisplayNameList TEXT,
// ChatRoomFlag int Default 0,
// Owner INTEGER DEFAULT 0,
// IsShowName INTEGER DEFAULT 0,
// SelfDisplayName TEXT,
// Reserved1 INTEGER DEFAULT 0,
// Reserved2 TEXT,
// Reserved3 INTEGER DEFAULT 0,
// Reserved4 TEXT,
// Reserved5 INTEGER DEFAULT 0,
// Reserved6 TEXT,
// RoomData BLOB,
// Reserved7 INTEGER DEFAULT 0,
// Reserved8 TEXT
// )
type ChatRoomV3 struct {
	ChatRoomName string `json:"ChatRoomName"`
	Reserved2    string `json:"Reserved2"` // Creator
	RoomData     []byte `json:"RoomData"`

	// // 非关键信息，暂时忽略
	// UserNameList    string `json:"UserNameList"`
	// DisplayNameList string `json:"DisplayNameList"`
	// ChatRoomFlag    int    `json:"ChatRoomFlag"`
	// Owner           int    `json:"Owner"`
	// IsShowName      int    `json:"IsShowName"`
	// SelfDisplayName string `json:"SelfDisplayName"`
	// Reserved1       int    `json:"Reserved1"`
	// Reserved3       int    `json:"Reserved3"`
	// Reserved4       string `json:"Reserved4"`
	// Reserved5       int    `json:"Reserved5"`
	// Reserved6       string `json:"Reserved6"`
	// Reserved7       int    `json:"Reserved7"`
	// Reserved8       string `json:"Reserved8"`
}

func (c *ChatRoomV3) Wrap() *ChatRoom {

	var users []ChatRoomUser
	if len(c.RoomData) != 0 {
		users = ParseRoomData(c.RoomData)
	}

	user2DisplayName := make(map[string]string, len(users))
	for _, user := range users {
		if user.DisplayName != "" {
			user2DisplayName[user.UserName] = user.DisplayName
		}
	}

	return &ChatRoom{
		Name:             c.ChatRoomName,
		Owner:            c.Reserved2,
		Users:            users,
		User2DisplayName: user2DisplayName,
	}
}

func ParseRoomData(b []byte) (users []ChatRoomUser) {
	var pbMsg wxproto.RoomData
	if err := proto.Unmarshal(b, &pbMsg); err != nil {
		return
	}
	if pbMsg.Users == nil {
		return
	}

	users = make([]ChatRoomUser, 0, len(pbMsg.Users))
	for _, user := range pbMsg.Users {
		u := ChatRoomUser{UserName: user.UserName}
		if user.DisplayName != nil {
			u.DisplayName = *user.DisplayName
		}
		users = append(users, u)
	}
	return users
}

func (c *ChatRoom) DisplayName() string {
	switch {
	case c.Remark != "":
		return c.Remark
	case c.NickName != "":
		return c.NickName
	}
	return ""
}

package repository

import (
	"context"
	"sort"
	"strings"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
)

// initContactCache 初始化联系人缓存
func (r *Repository) initContactCache(ctx context.Context) error {
	// 加载所有联系人到缓存
	contacts, err := r.ds.GetContacts(ctx, "", 0, 0)
	if err != nil {
		return err
	}

	contactMap := make(map[string]*model.Contact)
	aliasMap := make(map[string][]*model.Contact)
	remarkMap := make(map[string][]*model.Contact)
	nickNameMap := make(map[string][]*model.Contact)
	chatRoomUserMap := make(map[string]*model.Contact)
	chatRoomInContactMap := make(map[string]*model.Contact)
	contactList := make([]string, 0)
	aliasList := make([]string, 0)
	remarkList := make([]string, 0)
	nickNameList := make([]string, 0)

	for _, contact := range contacts {
		contactMap[contact.UserName] = contact
		contactList = append(contactList, contact.UserName)

		// 建立快速查找索引
		if contact.Alias != "" {
			alias, ok := aliasMap[contact.Alias]
			if !ok {
				alias = make([]*model.Contact, 0)
			}
			alias = append(alias, contact)
			aliasMap[contact.Alias] = alias
			aliasList = append(aliasList, contact.Alias)
		}
		if contact.Remark != "" {
			remark, ok := remarkMap[contact.Remark]
			if !ok {
				remark = make([]*model.Contact, 0)
			}
			remark = append(remark, contact)
			remarkMap[contact.Remark] = remark
			remarkList = append(remarkList, contact.Remark)
		}
		if contact.NickName != "" {
			nickName, ok := nickNameMap[contact.NickName]
			if !ok {
				nickName = make([]*model.Contact, 0)
			}
			nickName = append(nickName, contact)
			nickNameMap[contact.NickName] = nickName
			nickNameList = append(nickNameList, contact.NickName)
		}

		// 如果是群聊成员（非好友），添加到群聊成员索引
		if !contact.IsFriend {
			chatRoomUserMap[contact.UserName] = contact
		}

		if strings.HasSuffix(contact.UserName, "@chatroom") {
			chatRoomInContactMap[contact.UserName] = contact
		}
	}

	sort.Strings(contactList)
	sort.Strings(aliasList)
	sort.Strings(remarkList)
	sort.Strings(nickNameList)

	r.contactCache = contactMap
	r.aliasToContact = aliasMap
	r.remarkToContact = remarkMap
	r.nickNameToContact = nickNameMap
	r.chatRoomUserToInfo = chatRoomUserMap
	r.chatRoomInContact = chatRoomInContactMap
	r.contactList = contactList
	r.aliasList = aliasList
	r.remarkList = remarkList
	r.nickNameList = nickNameList
	return nil
}

func (r *Repository) GetContact(ctx context.Context, key string) (*model.Contact, error) {
	// 先尝试从缓存中获取
	contact := r.findContact(key)
	if contact == nil {
		return nil, errors.ContactNotFound(key)
	}
	return contact, nil
}

func (r *Repository) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
	ret := make([]*model.Contact, 0)
	if key != "" {
		ret = r.findContacts(key)
		if len(ret) == 0 {
			return []*model.Contact{}, nil
		}
		if limit > 0 {
			end := offset + limit
			if end > len(ret) {
				end = len(ret)
			}
			if offset >= len(ret) {
				return []*model.Contact{}, nil
			}
			return ret[offset:end], nil
		}
	} else {
		list := r.contactList
		if limit > 0 {
			end := offset + limit
			if end > len(list) {
				end = len(list)
			}
			if offset >= len(list) {
				return []*model.Contact{}, nil
			}
			list = list[offset:end]
		}
		for _, name := range list {
			ret = append(ret, r.contactCache[name])
		}
	}
	return ret, nil
}

func (r *Repository) findContact(key string) *model.Contact {
	if contact, ok := r.contactCache[key]; ok {
		return contact
	}
	if contact, ok := r.aliasToContact[key]; ok {
		return contact[0]
	}
	if contact, ok := r.remarkToContact[key]; ok {
		return contact[0]
	}
	if contact, ok := r.nickNameToContact[key]; ok {
		return contact[0]
	}

	// Contain
	for _, alias := range r.aliasList {
		if strings.Contains(alias, key) {
			return r.aliasToContact[alias][0]
		}
	}
	for _, remark := range r.remarkList {
		if strings.Contains(remark, key) {
			return r.remarkToContact[remark][0]
		}
	}
	for _, nickName := range r.nickNameList {
		if strings.Contains(nickName, key) {
			return r.nickNameToContact[nickName][0]
		}
	}
	return nil
}

func (r *Repository) findContacts(key string) []*model.Contact {
	ret := make([]*model.Contact, 0)
	distinct := make(map[string]bool)
	if contact, ok := r.contactCache[key]; ok {
		ret = append(ret, contact)
		distinct[contact.UserName] = true
	}
	if contacts, ok := r.aliasToContact[key]; ok {
		for _, contact := range contacts {
			if !distinct[contact.UserName] {
				ret = append(ret, contact)
				distinct[contact.UserName] = true
			}
		}
	}
	if contacts, ok := r.remarkToContact[key]; ok {
		for _, contact := range contacts {
			if !distinct[contact.UserName] {
				ret = append(ret, contact)
				distinct[contact.UserName] = true
			}
		}
	}
	if contacts, ok := r.nickNameToContact[key]; ok {
		for _, contact := range contacts {
			if !distinct[contact.UserName] {
				ret = append(ret, contact)
				distinct[contact.UserName] = true
			}
		}
	}
	// Contain
	for _, alias := range r.aliasList {
		if strings.Contains(alias, key) {
			for _, contact := range r.aliasToContact[alias] {
				if !distinct[contact.UserName] {
					ret = append(ret, contact)
					distinct[contact.UserName] = true
				}
			}
		}
	}
	for _, remark := range r.remarkList {
		if strings.Contains(remark, key) {
			for _, contact := range r.remarkToContact[remark] {
				if !distinct[contact.UserName] {
					ret = append(ret, contact)
					distinct[contact.UserName] = true
				}
			}
		}
	}
	for _, nickName := range r.nickNameList {
		if strings.Contains(nickName, key) {
			for _, contact := range r.nickNameToContact[nickName] {
				if !distinct[contact.UserName] {
					ret = append(ret, contact)
					distinct[contact.UserName] = true
				}
			}
		}
	}

	return ret
}

// getFullContact 获取联系人信息，包括群聊成员
func (r *Repository) getFullContact(userName string) *model.Contact {
	// 先查找联系人缓存
	if contact, ok := r.contactCache[userName]; ok {
		return contact
	}

	// 再查找群聊成员缓存
	contact, ok := r.chatRoomUserToInfo[userName]

	if ok {
		return contact
	}

	return nil
}

package repository

import (
	"context"
	"sort"
	"strings"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
)

// initChatRoomCache 初始化群聊缓存
func (r *Repository) initChatRoomCache(ctx context.Context) error {
	// 加载所有群聊到缓存
	chatRooms, err := r.ds.GetChatRooms(ctx, "", 0, 0)
	if err != nil {
		return err
	}

	chatRoomMap := make(map[string]*model.ChatRoom)
	remarkToChatRoom := make(map[string][]*model.ChatRoom)
	nickNameToChatRoom := make(map[string][]*model.ChatRoom)
	chatRoomList := make([]string, 0)
	chatRoomRemark := make([]string, 0)
	chatRoomNickName := make([]string, 0)

	for _, chatRoom := range chatRooms {
		// 补充群聊信息（从联系人中获取 Remark 和 NickName）
		r.enrichChatRoom(chatRoom)
		chatRoomMap[chatRoom.Name] = chatRoom
		chatRoomList = append(chatRoomList, chatRoom.Name)
		if chatRoom.Remark != "" {
			remark, ok := remarkToChatRoom[chatRoom.Remark]
			if !ok {
				remark = make([]*model.ChatRoom, 0)
			}
			remark = append(remark, chatRoom)
			remarkToChatRoom[chatRoom.Remark] = remark
			chatRoomRemark = append(chatRoomRemark, chatRoom.Remark)
		}
		if chatRoom.NickName != "" {
			nickName, ok := nickNameToChatRoom[chatRoom.NickName]
			if !ok {
				nickName = make([]*model.ChatRoom, 0)
			}
			nickName = append(nickName, chatRoom)
			nickNameToChatRoom[chatRoom.NickName] = nickName
			chatRoomNickName = append(chatRoomNickName, chatRoom.NickName)
		}
	}

	for _, contact := range r.chatRoomInContact {
		if _, ok := chatRoomMap[contact.UserName]; !ok {
			chatRoom := &model.ChatRoom{
				Name:     contact.UserName,
				Remark:   contact.Remark,
				NickName: contact.NickName,
			}
			chatRoomMap[contact.UserName] = chatRoom
			chatRoomList = append(chatRoomList, contact.UserName)
			if contact.Remark != "" {
				remark, ok := remarkToChatRoom[chatRoom.Remark]
				if !ok {
					remark = make([]*model.ChatRoom, 0)
				}
				remark = append(remark, chatRoom)
				remarkToChatRoom[chatRoom.Remark] = remark
				chatRoomRemark = append(chatRoomRemark, contact.Remark)
			}
			if contact.NickName != "" {
				nickName, ok := nickNameToChatRoom[chatRoom.NickName]
				if !ok {
					nickName = make([]*model.ChatRoom, 0)
				}
				nickName = append(nickName, chatRoom)
				nickNameToChatRoom[chatRoom.NickName] = nickName
				chatRoomNickName = append(chatRoomNickName, contact.NickName)
			}
		}
	}
	sort.Strings(chatRoomList)
	sort.Strings(chatRoomRemark)
	sort.Strings(chatRoomNickName)

	r.chatRoomCache = chatRoomMap
	r.remarkToChatRoom = remarkToChatRoom
	r.nickNameToChatRoom = nickNameToChatRoom
	r.chatRoomList = chatRoomList
	r.chatRoomRemark = chatRoomRemark
	r.chatRoomNickName = chatRoomNickName

	return nil
}

func (r *Repository) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {

	ret := make([]*model.ChatRoom, 0)
	if key != "" {
		ret = r.findChatRooms(key)
		if len(ret) == 0 {
			return []*model.ChatRoom{}, nil
		}

		if limit > 0 {
			end := offset + limit
			if end > len(ret) {
				end = len(ret)
			}
			if offset >= len(ret) {
				return []*model.ChatRoom{}, nil
			}
			return ret[offset:end], nil
		}
	} else {
		list := r.chatRoomList
		if limit > 0 {
			end := offset + limit
			if end > len(list) {
				end = len(list)
			}
			if offset >= len(list) {
				return []*model.ChatRoom{}, nil
			}
			list = list[offset:end]
		}
		for _, name := range list {
			ret = append(ret, r.chatRoomCache[name])
		}
	}

	return ret, nil
}

func (r *Repository) GetChatRoom(ctx context.Context, key string) (*model.ChatRoom, error) {
	chatRoom := r.findChatRoom(key)
	if chatRoom == nil {
		return nil, errors.ChatRoomNotFound(key)
	}
	return chatRoom, nil
}

// enrichChatRoom 从联系人信息中补充群聊信息
func (r *Repository) enrichChatRoom(chatRoom *model.ChatRoom) {
	if contact, ok := r.contactCache[chatRoom.Name]; ok {
		chatRoom.Remark = contact.Remark
		chatRoom.NickName = contact.NickName
	}
}

func (r *Repository) findChatRoom(key string) *model.ChatRoom {
	if chatRoom, ok := r.chatRoomCache[key]; ok {
		return chatRoom
	}
	if chatRoom, ok := r.remarkToChatRoom[key]; ok {
		return chatRoom[0]
	}
	if chatRoom, ok := r.nickNameToChatRoom[key]; ok {
		return chatRoom[0]
	}

	// Contain
	for _, remark := range r.chatRoomRemark {
		if strings.Contains(remark, key) {
			return r.remarkToChatRoom[remark][0]
		}
	}
	for _, nickName := range r.chatRoomNickName {
		if strings.Contains(nickName, key) {
			return r.nickNameToChatRoom[nickName][0]
		}
	}

	return nil
}

func (r *Repository) findChatRooms(key string) []*model.ChatRoom {
	ret := make([]*model.ChatRoom, 0)
	distinct := make(map[string]bool)
	if chatRoom, ok := r.chatRoomCache[key]; ok {
		ret = append(ret, chatRoom)
		distinct[chatRoom.Name] = true
	}
	if chatRooms, ok := r.remarkToChatRoom[key]; ok {
		for _, chatRoom := range chatRooms {
			if !distinct[chatRoom.Name] {
				ret = append(ret, chatRoom)
				distinct[chatRoom.Name] = true
			}
		}
	}
	if chatRooms, ok := r.nickNameToChatRoom[key]; ok {
		for _, chatRoom := range chatRooms {
			if !distinct[chatRoom.Name] {
				ret = append(ret, chatRoom)
				distinct[chatRoom.Name] = true
			}
		}
	}

	// Contain
	for _, remark := range r.chatRoomRemark {
		if strings.Contains(remark, key) {
			for _, chatRoom := range r.remarkToChatRoom[remark] {
				if !distinct[chatRoom.Name] {
					ret = append(ret, chatRoom)
					distinct[chatRoom.Name] = true
				}
			}
		}
	}
	for _, nickName := range r.chatRoomNickName {
		if strings.Contains(nickName, key) {
			for _, chatRoom := range r.nickNameToChatRoom[nickName] {
				if !distinct[chatRoom.Name] {
					ret = append(ret, chatRoom)
					distinct[chatRoom.Name] = true
				}
			}
		}
	}

	return ret
}

package model

import (
	"encoding/xml"
	"fmt"
	"regexp"
	"strings"
)

type MediaMsg struct {
	XMLName xml.Name `xml:"msg"`
	Image   Image    `xml:"img,omitempty"`
	Video   Video    `xml:"videomsg,omitempty"`
	App     App      `xml:"appmsg,omitempty"`
}

type Image struct {
	MD5 string `xml:"md5,attr"`
	// HdLength            string `xml:"hdlength,attr"`
	// Length              string `xml:"length,attr"`
	// AesKey              string `xml:"aeskey,attr"`
	// EncryVer            string `xml:"encryver,attr"`
	// OriginSourceMd5     string `xml:"originsourcemd5,attr"`
	// FileKey             string `xml:"filekey,attr"`
	// UploadContinueCount string `xml:"uploadcontinuecount,attr"`
	// ImgSourceUrl        string `xml:"imgsourceurl,attr"`
	// HevcMidSize         string `xml:"hevc_mid_size,attr"`
	// CdnBigImgUrl        string `xml:"cdnbigimgurl,attr"`
	// CdnMidImgUrl        string `xml:"cdnmidimgurl,attr"`
	// CdnThumbUrl         string `xml:"cdnthumburl,attr"`
	// CdnThumbLength      string `xml:"cdnthumblength,attr"`
	// CdnThumbWidth       string `xml:"cdnthumbwidth,attr"`
	// CdnThumbHeight      string `xml:"cdnthumbheight,attr"`
	// CdnThumbAesKey      string `xml:"cdnthumbaeskey,attr"`
}

type Video struct {
	Md5    string `xml:"md5,attr"`
	RawMd5 string `xml:"rawmd5,attr"`
	// Length            string `xml:"length,attr"`
	// PlayLength        string `xml:"playlength,attr"`
	// Offset            string `xml:"offset,attr"`
	// FromUserName      string `xml:"fromusername,attr"`
	// Status            string `xml:"status,attr"`
	// Compress          string `xml:"compress,attr"`
	// CameraType        string `xml:"cameratype,attr"`
	// Source            string `xml:"source,attr"`
	// AesKey            string `xml:"aeskey,attr"`
	// CdnVideoUrl       string `xml:"cdnvideourl,attr"`
	// CdnThumbUrl       string `xml:"cdnthumburl,attr"`
	// CdnThumbLength    string `xml:"cdnthumblength,attr"`
	// CdnThumbWidth     string `xml:"cdnthumbwidth,attr"`
	// CdnThumbHeight    string `xml:"cdnthumbheight,attr"`
	// CdnThumbAesKey    string `xml:"cdnthumbaeskey,attr"`
	// EncryVer          string `xml:"encryver,attr"`
	// RawLength         string `xml:"rawlength,attr"`
	// CdnRawVideoUrl    string `xml:"cdnrawvideourl,attr"`
	// CdnRawVideoAesKey string `xml:"cdnrawvideoaeskey,attr"`
}

type App struct {
	Type              int         `xml:"type"`
	Title             string      `xml:"title"`
	Des               string      `xml:"des"`
	URL               string      `xml:"url"`                         // type 5 分享
	AppAttach         *AppAttach  `xml:"appattach,omitempty"`         // type 6 文件
	MD5               string      `xml:"md5,omitempty"`               // type 6 文件
	RecordItem        *RecordItem `xml:"recorditem,omitempty"`        // type 19 合并转发
	SourceDisplayName string      `xml:"sourcedisplayname,omitempty"` // type 33 小程序
	FinderFeed        *FinderFeed `xml:"finderFeed,omitempty"`        // type 51 视频号
	ReferMsg          *ReferMsg   `xml:"refermsg,omitempty"`          // type 57 引用
	PatMsg            *PatMsg     `xml:"patMsg,omitempty"`            // type 62 拍一拍
	WCPayInfo         *WCPayInfo  `xml:"wcpayinfo,omitempty"`         // type 2000 微信转账
}

// ReferMsg 表示引用消息
type ReferMsg struct {
	Type        int64  `xml:"type"`
	SvrID       string `xml:"svrid"`
	FromUsr     string `xml:"fromusr"`
	ChatUsr     string `xml:"chatusr"`
	DisplayName string `xml:"displayname"`
	MsgSource   string `xml:"msgsource"`
	Content     string `xml:"content"`
	StrID       string `xml:"strid"`
	CreateTime  int64  `xml:"createtime"`
}

// AppAttach 表示应用附件
type AppAttach struct {
	TotalLen       string `xml:"totallen"`
	AttachID       string `xml:"attachid"`
	CDNAttachURL   string `xml:"cdnattachurl"`
	EmoticonMD5    string `xml:"emoticonmd5"`
	AESKey         string `xml:"aeskey"`
	FileExt        string `xml:"fileext"`
	IsLargeFileMsg string `xml:"islargefilemsg"`
}

type RecordItem struct {
	CDATA string `xml:",cdata"`

	// 解析后的记录信息
	RecordInfo *RecordInfo
}

// RecordInfo 表示聊天记录信息
type RecordInfo struct {
	XMLName       xml.Name `xml:"recordinfo"`
	FromScene     string   `xml:"fromscene,omitempty"`
	FavUsername   string   `xml:"favusername,omitempty"`
	FavCreateTime string   `xml:"favcreatetime,omitempty"`
	IsChatRoom    string   `xml:"isChatRoom,omitempty"`
	Title         string   `xml:"title,omitempty"`
	Desc          string   `xml:"desc,omitempty"`
	Info          string   `xml:"info,omitempty"`
	DataList      DataList `xml:"datalist,omitempty"`
}

// DataList 表示数据列表
type DataList struct {
	Count     string     `xml:"count,attr,omitempty"`
	DataItems []DataItem `xml:"dataitem,omitempty"`
}

// DataItem 表示数据项
type DataItem struct {
	DataType      string `xml:"datatype,attr,omitempty"`
	DataID        string `xml:"dataid,attr,omitempty"`
	HTMLID        string `xml:"htmlid,attr,omitempty"`
	DataFmt       string `xml:"datafmt,omitempty"`
	SourceName    string `xml:"sourcename,omitempty"`
	SourceTime    string `xml:"sourcetime,omitempty"`
	SourceHeadURL string `xml:"sourceheadurl,omitempty"`
	DataDesc      string `xml:"datadesc,omitempty"`

	// 图片特有字段
	ThumbSourcePath  string `xml:"thumbsourcepath,omitempty"`
	ThumbSize        string `xml:"thumbsize,omitempty"`
	CDNDataURL       string `xml:"cdndataurl,omitempty"`
	CDNDataKey       string `xml:"cdndatakey,omitempty"`
	CDNThumbURL      string `xml:"cdnthumburl,omitempty"`
	CDNThumbKey      string `xml:"cdnthumbkey,omitempty"`
	DataSourcePath   string `xml:"datasourcepath,omitempty"`
	FullMD5          string `xml:"fullmd5,omitempty"`
	ThumbFullMD5     string `xml:"thumbfullmd5,omitempty"`
	ThumbHead256MD5  string `xml:"thumbhead256md5,omitempty"`
	DataSize         string `xml:"datasize,omitempty"`
	CDNEncryVer      string `xml:"cdnencryver,omitempty"`
	SrcChatname      string `xml:"srcChatname,omitempty"`
	SrcMsgLocalID    string `xml:"srcMsgLocalid,omitempty"`
	SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"`
	MessageUUID      string `xml:"messageuuid,omitempty"`
	FromNewMsgID     string `xml:"fromnewmsgid,omitempty"`

	// 套娃合并转发
	DataTitle string     `xml:"datatitle,omitempty"`
	RecordXML *RecordXML `xml:"recordxml,omitempty"`
}

type RecordXML struct {
	RecordInfo RecordInfo `xml:"recordinfo,omitempty"`
}

func (r *RecordInfo) String(title, host string) string {
	buf := strings.Builder{}
	if title == "" {
		title = r.Title
	}
	buf.WriteString(fmt.Sprintf("[合并转发|%s]\n", title))
	for _, item := range r.DataList.DataItems {
		buf.WriteString(fmt.Sprintf("  %s %s\n", item.SourceName, item.SourceTime))

		// 套娃合并转发
		if item.DataType == "17" && item.RecordXML != nil {
			content := item.RecordXML.RecordInfo.String(item.DataTitle, host)
			if content != "" {
				for _, line := range strings.Split(content, "\n") {
					buf.WriteString(fmt.Sprintf("  %s\n", line))
				}
			}
			continue
		}

		switch item.DataFmt {
		case "pic", "jpg":
			buf.WriteString(fmt.Sprintf("  ![图片](http://%s/image/%s)\n", host, item.FullMD5))
		default:
			for _, line := range strings.Split(item.DataDesc, "\n") {
				buf.WriteString(fmt.Sprintf("  %s\n", line))
			}
		}
		buf.WriteString("\n")
	}
	return buf.String()
}

// PatMsg 拍一拍消息结构
type PatMsg struct {
	ChatUser  string  `xml:"chatUser"`  // 被拍的用户
	RecordNum int     `xml:"recordNum"` // 记录数量
	Records   Records `xml:"records"`   // 拍一拍记录
}

// Records 拍一拍记录集合
type Records struct {
	Record []PatRecord `xml:"record"` // 拍一拍记录列表
}

// PatRecord 单条拍一拍记录
type PatRecord struct {
	FromUser   string `xml:"fromUser"`   // 发起拍一拍的用户
	PattedUser string `xml:"pattedUser"` // 被拍的用户
	Templete   string `xml:"templete"`   // 模板文本
	CreateTime int64  `xml:"createTime"` // 创建时间
	SvrId      string `xml:"svrId"`      // 服务器ID
	ReadStatus int    `xml:"readStatus"` // 已读状态
}

// WCPayInfo 微信支付信息
type WCPayInfo struct {
	PaySubType        int    `xml:"paysubtype"`        // 支付子类型
	FeeDesc           string `xml:"feedesc"`           // 金额描述，如"￥200000.00"
	TranscationID     string `xml:"transcationid"`     // 交易ID
	TransferID        string `xml:"transferid"`        // 转账ID
	InvalidTime       string `xml:"invalidtime"`       // 失效时间
	BeginTransferTime string `xml:"begintransfertime"` // 开始转账时间
	EffectiveDate     string `xml:"effectivedate"`     // 生效日期
	PayMemo           string `xml:"pay_memo"`          // 支付备注
	ReceiverUsername  string `xml:"receiver_username"` // 接收方用户名
	PayerUsername     string `xml:"payer_username"`    // 支付方用户名
}

// FinderFeed 视频号信息
type FinderFeed struct {
	ObjectID            string          `xml:"objectId"`
	FeedType            string          `xml:"feedType"`
	Nickname            string          `xml:"nickname"`
	Avatar              string          `xml:"avatar"`
	Desc                string          `xml:"desc"`
	MediaCount          string          `xml:"mediaCount"`
	ObjectNonceID       string          `xml:"objectNonceId"`
	LiveID              string          `xml:"liveId"`
	Username            string          `xml:"username"`
	AuthIconURL         string          `xml:"authIconUrl"`
	AuthIconType        int             `xml:"authIconType"`
	ContactJumpInfoStr  string          `xml:"contactJumpInfoStr"`
	SourceCommentScene  int             `xml:"sourceCommentScene"`
	MediaList           FinderMediaList `xml:"mediaList"`
	MegaVideo           FinderMegaVideo `xml:"megaVideo"`
	BizUsername         string          `xml:"bizUsername"`
	BizNickname         string          `xml:"bizNickname"`
	BizAvatar           string          `xml:"bizAvatar"`
	BizUsernameV2       string          `xml:"bizUsernameV2"`
	BizAuthIconURL      string          `xml:"bizAuthIconUrl"`
	BizAuthIconType     int             `xml:"bizAuthIconType"`
	EcSource            string          `xml:"ecSource"`
	LastGMsgID          string          `xml:"lastGMsgID"`
	ShareBypData        string          `xml:"shareBypData"`
	IsDebug             int             `xml:"isDebug"`
	ContentType         int             `xml:"content_type"`
	FinderForwardSource string          `xml:"finderForwardSource"`
}

type FinderMediaList struct {
	Media []FinderMedia `xml:"media"`
}

type FinderMedia struct {
	ThumbURL          string `xml:"thumbUrl"`
	FullCoverURL      string `xml:"fullCoverUrl"`
	VideoPlayDuration string `xml:"videoPlayDuration"`
	URL               string `xml:"url"`
	CoverURL          string `xml:"coverUrl"`
	Height            string `xml:"height"`
	MediaType         string `xml:"mediaType"`
	FullClipInset     string `xml:"fullClipInset"`
	Width             string `xml:"width"`
}

type FinderMegaVideo struct {
	ObjectID      string `xml:"objectId"`
	ObjectNonceID string `xml:"objectNonceId"`
}

type SysMsg struct {
	Type              string             `xml:"type,attr"`
	DelChatRoomMember *DelChatRoomMember `xml:"delchatroommember,omitempty"`
	SysMsgTemplate    *SysMsgTemplate    `xml:"sysmsgtemplate,omitempty"`
}

// 第一种消息类型：删除群成员/二维码邀请
type DelChatRoomMember struct {
	Plain string `xml:"plain"`
	Text  string `xml:"text"`
	Link  QRLink `xml:"link"`
}

type QRLink struct {
	Scene      string       `xml:"scene"`
	Text       string       `xml:"text"`
	MemberList QRMemberList `xml:"memberlist"`
	QRCode     string       `xml:"qrcode"`
}

type QRMemberList struct {
	Usernames []UsernameItem `xml:"username"`
}

type UsernameItem struct {
	Value string `xml:",chardata"`
}

// 第二种消息类型：系统消息模板
type SysMsgTemplate struct {
	ContentTemplate ContentTemplate `xml:"content_template"`
}

type ContentTemplate struct {
	Type     string   `xml:"type,attr"`
	Plain    string   `xml:"plain"`
	Template string   `xml:"template"`
	LinkList LinkList `xml:"link_list"`
}

type LinkList struct {
	Links []Link `xml:"link"`
}

type Link struct {
	Name       string     `xml:"name,attr"`
	Type       string     `xml:"type,attr"`
	MemberList MemberList `xml:"memberlist"`
	Separator  string     `xml:"separator,omitempty"`
	Title      string     `xml:"title,omitempty"`
}

type MemberList struct {
	Members []Member `xml:"member"`
}

type Member struct {
	Username string `xml:"username"`
	Nickname string `xml:"nickname"`
}

func (s *SysMsg) String() string {
	if s.Type == "delchatroommember" {
		return s.DelChatRoomMemberString()
	}
	return s.SysMsgTemplateString()
}

func (s *SysMsg) DelChatRoomMemberString() string {
	if s.DelChatRoomMember == nil {
		return ""
	}
	return s.DelChatRoomMember.Plain
}

func (s *SysMsg) SysMsgTemplateString() string {
	if s.SysMsgTemplate == nil {
		return ""
	}

	template := s.SysMsgTemplate.ContentTemplate.Template
	links := s.SysMsgTemplate.ContentTemplate.LinkList.Links

	// 创建一个映射，用于存储占位符名称和对应的替换内容
	replacements := make(map[string]string)

	// 遍历所有链接，为每个占位符准备替换内容
	for _, link := range links {
		var replacement string

		// 根据链接类型和成员信息生成替换内容
		switch link.Type {
		case "link_profile":
			// 使用自定义分隔符，如果未指定则默认使用"、"
			separator := link.Separator
			if separator == "" {
				separator = "、"
			}

			// 处理成员信息，格式为 nickname(username)
			var memberTexts []string
			for _, member := range link.MemberList.Members {
				if member.Nickname != "" {
					memberText := member.Nickname
					if member.Username != "" {
						memberText += "(" + member.Username + ")"
					}
					memberTexts = append(memberTexts, memberText)
				}
			}

			// 使用指定的分隔符连接所有成员文本
			replacement = strings.Join(memberTexts, separator)

		// 可以根据需要添加其他链接类型的处理逻辑
		default:
			if link.Title != "" {
				replacement = link.Title
			} else {
				replacement = ""
			}
		}

		// 将占位符名称和替换内容存入映射
		replacements["$"+link.Name+"$"] = replacement
	}

	// 使用正则表达式查找并替换所有占位符
	re := regexp.MustCompile(`\$([^$]+)\$`)
	result := re.ReplaceAllStringFunc(template, func(match string) string {
		if replacement, ok := replacements[match]; ok {
			return replacement
		}
		// 如果找不到对应的替换内容，保留原占位符
		return match
	})

	return result
}

package windows

import (
	"path/filepath"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/shirou/gopsutil/v4/process"

	"github.com/sjzar/chatlog/internal/wechat/model"
)

// initializeProcessInfo 获取进程的数据目录和账户名
func initializeProcessInfo(p *process.Process, info *model.Process) error {
	files, err := p.OpenFiles()
	if err != nil {
		log.Err(err).Msgf("获取进程 %d 的打开文件失败", p.Pid)
		return err
	}

	dbPath := V3DBFile
	if info.Version == 4 {
		dbPath = V4DBFile
	}

	for _, f := range files {
		if strings.HasSuffix(f.Path, dbPath) {
			filePath := f.Path[4:] // 移除 "\\?\" 前缀
			parts := strings.Split(filePath, string(filepath.Separator))
			if len(parts) < 4 {
				log.Debug().Msg("无效的文件路径: " + filePath)
				continue
			}

			info.Status = model.StatusOnline
			if info.Version == 4 {
				info.DataDir = strings.Join(parts[:len(parts)-3], string(filepath.Separator))
				info.AccountName = parts[len(parts)-4]
			} else {
				info.DataDir = strings.Join(parts[:len(parts)-2], string(filepath.Separator))
				info.AccountName = parts[len(parts)-3]
			}
			return nil
		}
	}

	return nil
}

package errors

import (
	"net/http"
	"runtime/debug"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
)

// ErrorHandlerMiddleware 是一个 Gin 中间件，用于统一处理请求过程中的错误
// 它会为每个请求生成一个唯一的请求 ID，并在错误发生时将其添加到错误响应中
func ErrorHandlerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 生成请求 ID
		requestID := uuid.New().String()
		c.Set("RequestID", requestID)
		c.Header("X-Request-ID", requestID)

		// 处理请求
		c.Next()

		// 检查是否有错误
		if len(c.Errors) > 0 {
			// 获取第一个错误
			err := c.Errors[0].Err

			// 使用 Err 函数处理错误响应
			Err(c, err)

			// 已经处理过错误，不需要继续
			c.Abort()
		}
	}
}

// RecoveryMiddleware 是一个 Gin 中间件，用于从 panic 恢复并返回 500 错误
func RecoveryMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if r := recover(); r != nil {

				// 创建内部服务器错误
				var err *Error
				switch v := r.(type) {
				case error:
					err = New(v, http.StatusInternalServerError, "panic recovered")
				default:
					err = Newf(nil, http.StatusInternalServerError, "panic recovered: %v", r)
				}

				// 记录错误日志
				log.Err(err).Msgf("PANIC RECOVERED\n%s", string(debug.Stack()))

				// 返回 500 错误
				c.JSON(http.StatusInternalServerError, err)
				c.Abort()
			}
		}()

		c.Next()
	}
}

package mcp

import (
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
)

const (
	SSEPingIntervalS  = 30
	SSEMessageChanCap = 100
	SSEContentType    = "text/event-stream; charset=utf-8"
)

type SSEWriter struct {
	id string
	c  *gin.Context
}

func NewSSEWriter(c *gin.Context, id string) *SSEWriter {
	c.Writer.Header().Set("Content-Type", SSEContentType)
	c.Writer.Header().Set("Cache-Control", "no-cache")
	c.Writer.Header().Set("Connection", "keep-alive")
	c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
	c.Writer.Flush()

	w := &SSEWriter{
		id: id,
		c:  c,
	}
	w.WriteEndpoing()
	go w.ping()
	return w
}

func (w *SSEWriter) Write(p []byte) (n int, err error) {
	w.WriteMessage(string(p))
	return len(p), nil
}

func (w *SSEWriter) WriteMessage(data string) {
	w.WriteEvent("message", data)
}

func (w *SSEWriter) WriteEvent(event string, data string) {
	w.c.Writer.WriteString(fmt.Sprintf("event: %s\n", event))
	w.c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data))
	w.c.Writer.Flush()
}

func (w *SSEWriter) ping() {
	for {
		select {
		case <-time.After(time.Second * SSEPingIntervalS):
			w.writePing()
		case <-w.c.Request.Context().Done():
			return
		}
	}
}

// WriteEndpoing
// event: endpoint
// data: /message?sessionId=285d67ee-1c17-40d9-ab03-173d5ff48419
func (w *SSEWriter) WriteEndpoing() {
	w.c.Writer.WriteString(fmt.Sprintf("event: endpoint\n"))
	w.c.Writer.WriteString(fmt.Sprintf("data: /message?sessionId=%s\n\n", w.id))
	w.c.Writer.Flush()
}

// WritePing
// : ping - 2025-03-16 06:41:51.280928+00:00
func (w *SSEWriter) writePing() {
	w.c.Writer.WriteString(fmt.Sprintf(": ping - %s\n\n", time.Now().Format("2006-01-02 15:04:05.999999-07:00")))
}

// SSE Session
// 维持一个 SSE 连接的会话
// 会话中包含了 SSE 连接的 ID，事件通道，停止通道
// 事件通道用于发送事件，停止通道用于停止会话
// 需要轮询发送 ping 事件以保持连接
type SSESession struct {
	SessionID string
	Events    map[string]chan string
	Stop      chan bool

	c *gin.Context
}

func NewSSESession(c *gin.Context) *SSESession {
	return &SSESession{c: c}
}
func (s *SSESession) SendEvent(event string, data string) {
	s.c.SSEvent(event, data)
}

func (s *SSESession) Close() {
	close(s.Stop)
}

// Event
// request:
//   POST /messages?sesessionId=?
//   '{"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3}'
//
// response:
//   GET /sse
//   event: message
//   data: {"jsonrpc":"2.0","id":3,"result":{"prompts":[]}}

// {
// 	"jsonrpc": "2.0",
// 	"id": 1,
// 	"result": {
// 	  "tools": [
// 		{
// 		  "name": "get_alerts",
// 		  "description": "Get weather alerts for a US state.\n\n    Args:\n        state: Two-letter US state code (e.g. CA, NY)\n    ",
// 		  "inputSchema": {
// 			"properties": {
// 			  "state": {
// 				"title": "State",
// 				"type": "string"
// 			  }
// 			},
// 			"required": [
// 			  "state"
// 			],
// 			"title": "get_alertsArguments",
// 			"type": "object"
// 		  }
// 		},
// 		{
// 		  "name": "get_forecast",
// 		  "description": "Get weather forecast for a location.\n\n    Args:\n        latitude: Latitude of the location\n        longitude: Longitude of the location\n    ",
// 		  "inputSchema": {
// 			"properties": {
// 			  "latitude": {
// 				"title": "Latitude",
// 				"type": "number"
// 			  },
// 			  "longitude": {
// 				"title": "Longitude",
// 				"type": "number"
// 			  }
// 			},
// 			"required": [
// 			  "latitude",
// 			  "longitude"
// 			],
// 			"title": "get_forecastArguments",
// 			"type": "object"
// 		  }
// 		}
// 	  ]
// 	}
//   }

// PING

package dbm

import (
	"database/sql"
	"runtime"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/pkg/filecopy"
	"github.com/sjzar/chatlog/pkg/filemonitor"
)

type DBManager struct {
	path    string
	fm      *filemonitor.FileMonitor
	fgs     map[string]*filemonitor.FileGroup
	dbs     map[string]*sql.DB
	dbPaths map[string][]string
	mutex   sync.RWMutex
}

func NewDBManager(path string) *DBManager {
	return &DBManager{
		path:    path,
		fm:      filemonitor.NewFileMonitor(),
		fgs:     make(map[string]*filemonitor.FileGroup),
		dbs:     make(map[string]*sql.DB),
		dbPaths: make(map[string][]string),
	}
}

func (d *DBManager) AddGroup(g *Group) error {
	fg, err := filemonitor.NewFileGroup(g.Name, d.path, g.Pattern, g.BlackList)
	if err != nil {
		return err
	}
	fg.AddCallback(d.Callback)
	d.fm.AddGroup(fg)
	d.mutex.Lock()
	d.fgs[g.Name] = fg
	d.mutex.Unlock()
	return nil
}

func (d *DBManager) AddCallback(name string, callback func(event fsnotify.Event) error) error {
	d.mutex.RLock()
	fg, ok := d.fgs[name]
	d.mutex.RUnlock()
	if !ok {
		return errors.FileGroupNotFound(name)
	}
	fg.AddCallback(callback)
	return nil
}

func (d *DBManager) GetDB(name string) (*sql.DB, error) {
	dbPaths, err := d.GetDBPath(name)
	if err != nil {
		return nil, err
	}
	return d.OpenDB(dbPaths[0])
}

func (d *DBManager) GetDBs(name string) ([]*sql.DB, error) {
	dbPaths, err := d.GetDBPath(name)
	if err != nil {
		return nil, err
	}
	dbs := make([]*sql.DB, 0)
	for _, file := range dbPaths {
		db, err := d.OpenDB(file)
		if err != nil {
			return nil, err
		}
		dbs = append(dbs, db)
	}
	return dbs, nil
}

func (d *DBManager) GetDBPath(name string) ([]string, error) {
	d.mutex.RLock()
	dbPaths, ok := d.dbPaths[name]
	d.mutex.RUnlock()
	if !ok {
		d.mutex.RLock()
		fg, ok := d.fgs[name]
		d.mutex.RUnlock()
		if !ok {
			return nil, errors.FileGroupNotFound(name)
		}
		list, err := fg.List()
		if err != nil {
			return nil, errors.DBFileNotFound(d.path, fg.PatternStr, err)
		}
		if len(list) == 0 {
			return nil, errors.DBFileNotFound(d.path, fg.PatternStr, nil)
		}
		dbPaths = list
		d.mutex.Lock()
		d.dbPaths[name] = dbPaths
		d.mutex.Unlock()
	}
	return dbPaths, nil
}

func (d *DBManager) OpenDB(path string) (*sql.DB, error) {
	d.mutex.RLock()
	db, ok := d.dbs[path]
	d.mutex.RUnlock()
	if ok {
		return db, nil
	}
	var err error
	tempPath := path
	if runtime.GOOS == "windows" {
		tempPath, err = filecopy.GetTempCopy(path)
		if err != nil {
			log.Err(err).Msgf("获取临时拷贝文件 %s 失败", path)
			return nil, err
		}
	}
	db, err = sql.Open("sqlite3", tempPath)
	if err != nil {
		log.Err(err).Msgf("连接数据库 %s 失败", path)
		return nil, err
	}
	d.mutex.Lock()
	d.dbs[path] = db
	d.mutex.Unlock()
	return db, nil
}

func (d *DBManager) Callback(event fsnotify.Event) error {
	if !event.Op.Has(fsnotify.Create) {
		return nil
	}

	d.mutex.Lock()
	db, ok := d.dbs[event.Name]
	if ok {
		delete(d.dbs, event.Name)
		go func(db *sql.DB) {
			time.Sleep(time.Second * 5)
			db.Close()
		}(db)
	}
	d.mutex.Unlock()

	return nil
}

func (d *DBManager) Start() error {
	return d.fm.Start()
}

func (d *DBManager) Stop() error {
	return d.fm.Stop()
}

func (d *DBManager) Close() error {
	for _, db := range d.dbs {
		db.Close()
	}
	return d.fm.Stop()
}

package menu

import (
	"fmt"
	"sort"

	"github.com/sjzar/chatlog/internal/ui/style"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

const (
	// DialogPadding dialog inner paddign.
	DialogPadding = 3

	// DialogFormHeight dialog "Enter"/"Cancel" form height.
	DialogHelpHeight = 1

	// DialogMinWidth dialog min width.
	DialogMinWidth = 40

	// TableHeightOffset table height offset for border.
	TableHeightOffset = 3

	cmdWidthOffset = 6
)

type SubMenu struct {
	*tview.Box
	title         string
	layout        *tview.Flex
	table         *tview.Table
	width         int
	height        int
	items         []*Item
	cancelHandler func()
}

func NewSubMenu(title string) *SubMenu {
	subMenu := &SubMenu{
		Box:    tview.NewBox(),
		title:  title,
		items:  make([]*Item, 0),
		layout: tview.NewFlex(),
		table:  tview.NewTable(),
	}

	subMenu.table.SetBorders(false)
	subMenu.table.SetSelectable(true, false)
	subMenu.table.SetBorderColor(style.DialogBorderColor)
	subMenu.table.SetBackgroundColor(style.DialogBgColor)
	subMenu.table.SetTitleColor(style.DialogFgColor)
	subMenu.table.SetFixed(1, 1)

	subMenu.table.Select(1, 0).SetSelectedFunc(func(row, column int) {
		if row == 0 {
			return // 忽略表头
		}

		item := subMenu.items[row-1]
		if item.Selected != nil {
			item.Selected(item)
		}
	})

	subMenu.setTableHeader()

	// 帮助信息
	helpText := tview.NewTextView()
	helpText.SetDynamicColors(true)
	helpText.SetTextAlign(tview.AlignCenter)
	helpText.SetTextColor(style.DialogFgColor)
	helpText.SetBackgroundColor(style.DialogBgColor)
	fmt.Fprintf(helpText,
		"[%s::b]↑/↓[%s::b]: 导航  [%s::b]Enter[%s::b]: 选择  [%s::b]ESC[%s::b]: 返回",
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
	)

	// 布局
	tableLayout := tview.NewFlex().SetDirection(tview.FlexColumn)
	tableLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, true)
	tableLayout.AddItem(subMenu.table, 0, 1, true)
	tableLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, true)

	subMenu.layout.SetDirection(tview.FlexRow)
	subMenu.layout.SetTitle(fmt.Sprintf("[::b]%s", subMenu.title))
	subMenu.layout.SetTitleColor(style.DialogFgColor)
	subMenu.layout.SetTitleAlign(tview.AlignCenter)
	subMenu.layout.AddItem(tableLayout, 0, 1, true)
	subMenu.layout.AddItem(helpText, DialogHelpHeight, 0, true)
	subMenu.layout.SetBorder(true)
	subMenu.layout.SetBorderColor(style.DialogBorderColor)
	subMenu.layout.SetBackgroundColor(style.DialogBgColor)

	return subMenu
}

func (m *SubMenu) setTableHeader() {
	m.table.SetCell(0, 0, tview.NewTableCell(fmt.Sprintf("[%s::b]%s", style.GetColorHex(style.TableHeaderFgColor), "命令")).
		SetExpansion(1).
		SetBackgroundColor(style.TableHeaderBgColor).
		SetTextColor(style.TableHeaderFgColor).
		SetAlign(tview.AlignLeft).
		SetSelectable(false))

	m.table.SetCell(0, 1, tview.NewTableCell(fmt.Sprintf("[%s::b]%s", style.GetColorHex(style.TableHeaderFgColor), "说明")).
		SetExpansion(1).
		SetBackgroundColor(style.TableHeaderBgColor).
		SetTextColor(style.TableHeaderFgColor).
		SetAlign(tview.AlignLeft).
		SetSelectable(false))
}

func (m *SubMenu) AddItem(item *Item) {
	m.items = append(m.items, item)
	sort.Sort(SortItems(m.items))
	m.refresh()
}

func (m *SubMenu) SetItems(items []*Item) {
	m.items = items
	m.refresh()
}

func (m *SubMenu) SetCancelFunc(handler func()) *SubMenu {
	m.cancelHandler = handler
	return m
}

func (m *SubMenu) refresh() {
	m.table.Clear()
	m.setTableHeader()

	col1Width := 0
	col2Width := 0

	row := 1
	for _, item := range m.items {
		if item.Hidden {
			continue
		}
		m.table.SetCell(row, 0, tview.NewTableCell(item.Name).
			SetTextColor(style.DialogFgColor).
			SetBackgroundColor(style.DialogBgColor).
			SetReference(item).
			SetAlign(tview.AlignLeft))
		m.table.SetCell(row, 1, tview.NewTableCell(item.Description).
			SetTextColor(style.DialogFgColor).
			SetBackgroundColor(style.DialogBgColor).
			SetReference(item).
			SetAlign(tview.AlignLeft))
		if len(item.Name) > col1Width {
			col1Width = len(item.Name)
		}
		if len(item.Description) > col2Width {
			col2Width = len(item.Description)
		}
		row++
	}

	m.width = col1Width + col2Width + 2 + cmdWidthOffset
	m.height = len(m.items) + TableHeightOffset + DialogHelpHeight + 1

}

func (m *SubMenu) Draw(screen tcell.Screen) {
	m.refresh()

	m.Box.DrawForSubclass(screen, m)
	m.layout.Draw(screen)
}

func (m *SubMenu) SetRect(x, y, width, height int) {
	ws := (width - m.width) / 2
	hs := ((height - m.height) / 2)
	dy := y + hs
	bWidth := m.width

	if m.width > width {
		ws = 0
		bWidth = width - 1
	}

	bHeight := m.height

	if m.height >= height {
		dy = y + 1
		bHeight = height - 1
	}

	m.Box.SetRect(x+ws, dy, bWidth, bHeight)

	x, y, width, height = m.Box.GetInnerRect()

	m.layout.SetRect(x, y, width, height)
}

func (m *SubMenu) Focus(delegate func(p tview.Primitive)) {
	delegate(m.table)
}

// HasFocus returns whether or not this primitive has focus
func (m *SubMenu) HasFocus() bool {
	// Check if the active menu has focus
	return m.table.HasFocus()
}

func (m *SubMenu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
	return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {

		if event.Key() == tcell.KeyEscape && m.cancelHandler != nil {
			m.cancelHandler()
			return
		}

		// 将事件传递给表格
		if handler := m.table.InputHandler(); handler != nil {
			handler(event, setFocus)
		}
	})
}

func EmptyBoxSpace(bgColor tcell.Color) *tview.Box {
	box := tview.NewBox()
	box.SetBackgroundColor(bgColor)
	box.SetBorder(false)

	return box
}

package repository

import (
	"context"

	"github.com/fsnotify/fsnotify"
	"github.com/rs/zerolog/log"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource"
)

// Repository 实现了 repository.Repository 接口
type Repository struct {
	ds datasource.DataSource

	// Cache for contact
	contactCache      map[string]*model.Contact
	aliasToContact    map[string][]*model.Contact
	remarkToContact   map[string][]*model.Contact
	nickNameToContact map[string][]*model.Contact
	chatRoomInContact map[string]*model.Contact
	contactList       []string
	aliasList         []string
	remarkList        []string
	nickNameList      []string

	// Cache for chat room
	chatRoomCache      map[string]*model.ChatRoom
	remarkToChatRoom   map[string][]*model.ChatRoom
	nickNameToChatRoom map[string][]*model.ChatRoom
	chatRoomList       []string
	chatRoomRemark     []string
	chatRoomNickName   []string

	// 快速查找索引
	chatRoomUserToInfo map[string]*model.Contact
}

// New 创建一个新的 Repository
func New(ds datasource.DataSource) (*Repository, error) {
	r := &Repository{
		ds:                 ds,
		contactCache:       make(map[string]*model.Contact),
		aliasToContact:     make(map[string][]*model.Contact),
		remarkToContact:    make(map[string][]*model.Contact),
		nickNameToContact:  make(map[string][]*model.Contact),
		chatRoomUserToInfo: make(map[string]*model.Contact),
		contactList:        make([]string, 0),
		aliasList:          make([]string, 0),
		remarkList:         make([]string, 0),
		nickNameList:       make([]string, 0),
		chatRoomCache:      make(map[string]*model.ChatRoom),
		remarkToChatRoom:   make(map[string][]*model.ChatRoom),
		nickNameToChatRoom: make(map[string][]*model.ChatRoom),
		chatRoomList:       make([]string, 0),
		chatRoomRemark:     make([]string, 0),
		chatRoomNickName:   make([]string, 0),
	}

	// 初始化缓存
	if err := r.initCache(context.Background()); err != nil {
		return nil, errors.InitCacheFailed(err)
	}

	ds.SetCallback("contact", r.contactCallback)
	ds.SetCallback("chatroom", r.chatroomCallback)

	return r, nil
}

// initCache 初始化缓存
func (r *Repository) initCache(ctx context.Context) error {
	// 初始化联系人缓存
	if err := r.initContactCache(ctx); err != nil {
		return err
	}

	// 初始化群聊缓存
	if err := r.initChatRoomCache(ctx); err != nil {
		return err
	}

	return nil
}

func (r *Repository) contactCallback(event fsnotify.Event) error {
	if !event.Op.Has(fsnotify.Create) {
		return nil
	}
	if err := r.initContactCache(context.Background()); err != nil {
		log.Err(err).Msgf("Failed to reinitialize contact cache: %s", event.Name)
	}
	return nil
}

func (r *Repository) chatroomCallback(event fsnotify.Event) error {
	if !event.Op.Has(fsnotify.Create) {
		return nil
	}
	if err := r.initChatRoomCache(context.Background()); err != nil {
		log.Err(err).Msgf("Failed to reinitialize contact cache: %s", event.Name)
	}
	return nil
}

// Close 实现 Repository 接口的 Close 方法
func (r *Repository) Close() error {
	return r.ds.Close()
}

//go:build !windows
// +build !windows

package style

import (
	"fmt"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

const (
	// HeavyGreenCheckMark unicode.
	HeavyGreenCheckMark = "\u2705"
	// HeavyRedCrossMark unicode.
	HeavyRedCrossMark = "\u274C"
	// ProgressBar cell.
	ProgressBarCell = "▉"
)

var (
	// infobar.
	InfoBarItemFgColor = tcell.ColorSilver
	// main views.
	FgColor              = tcell.ColorFloralWhite
	BgColor              = tview.Styles.PrimitiveBackgroundColor
	BorderColor          = tcell.NewRGBColor(135, 175, 146) //nolint:mnd
	HelpHeaderFgColor    = tcell.NewRGBColor(135, 175, 146) //nolint:mnd
	MenuBgColor          = tcell.ColorMediumSeaGreen
	PageHeaderBgColor    = tcell.ColorMediumSeaGreen
	PageHeaderFgColor    = tcell.ColorFloralWhite
	RunningStatusFgColor = tcell.NewRGBColor(95, 215, 0)  //nolint:mnd
	PausedStatusFgColor  = tcell.NewRGBColor(255, 175, 0) //nolint:mnd
	// dialogs.
	DialogBgColor            = tcell.NewRGBColor(38, 38, 38) //nolint:mnd
	DialogBorderColor        = tcell.ColorMediumSeaGreen
	DialogFgColor            = tcell.ColorFloralWhite
	DialogSubBoxBorderColor  = tcell.ColorDimGray
	ErrorDialogBgColor       = tcell.NewRGBColor(215, 0, 0) //nolint:mnd
	ErrorDialogButtonBgColor = tcell.ColorDarkRed
	// terminal.
	TerminalFgColor     = tcell.ColorFloralWhite
	TerminalBgColor     = tcell.NewRGBColor(5, 5, 5) //nolint:mnd
	TerminalBorderColor = tcell.ColorDimGray
	// table header.
	TableHeaderBgColor = tcell.ColorMediumSeaGreen
	TableHeaderFgColor = tcell.ColorFloralWhite
	// progress bar.
	PrgBgColor       = tcell.ColorDimGray
	PrgBarColor      = tcell.ColorDarkOrange
	PrgBarEmptyColor = tcell.ColorWhite
	PrgBarOKColor    = tcell.ColorGreen
	PrgBarWarnColor  = tcell.ColorOrange
	PrgBarCritColor  = tcell.ColorRed
	// dropdown.
	DropDownUnselected = tcell.StyleDefault.Background(tcell.ColorWhiteSmoke).Foreground(tcell.ColorBlack)
	DropDownSelected   = tcell.StyleDefault.Background(tcell.ColorLightSlateGray).Foreground(tcell.ColorWhite)
	// other primitives.
	InputFieldBgColor = tcell.ColorGray
	ButtonBgColor     = tcell.ColorMediumSeaGreen
)

// GetColorName returns convert tcell color to its name.
func GetColorName(color tcell.Color) string {
	for name, c := range tcell.ColorNames {
		if c == color {
			return name
		}
	}

	return ""
}

// GetColorHex returns convert tcell color to its hex useful for textview primitives.
func GetColorHex(color tcell.Color) string {
	return fmt.Sprintf("#%x", color.Hex())
}

package key

import (
	"context"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt"
	"github.com/sjzar/chatlog/internal/wechat/key/darwin"
	"github.com/sjzar/chatlog/internal/wechat/key/windows"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

// Extractor 定义密钥提取器接口
type Extractor interface {
	// Extract 从进程中提取密钥
	Extract(ctx context.Context, proc *model.Process) (string, error)

	// SearchKey 在内存中搜索密钥
	SearchKey(ctx context.Context, memory []byte) (string, bool)

	SetValidate(validator *decrypt.Validator)
}

// NewExtractor 创建适合当前平台的密钥提取器
func NewExtractor(platform string, version int) (Extractor, error) {
	switch {
	case platform == "windows" && version == 3:
		return windows.NewV3Extractor(), nil
	case platform == "windows" && version == 4:
		return windows.NewV4Extractor(), nil
	case platform == "darwin" && version == 3:
		return darwin.NewV3Extractor(), nil
	case platform == "darwin" && version == 4:
		return darwin.NewV4Extractor(), nil
	default:
		return nil, errors.PlatformUnsupported(platform, version)
	}
}

package menu

import (
	"fmt"
	"sort"

	"github.com/sjzar/chatlog/internal/ui/style"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

type Item struct {
	Index       int
	Key         string
	Name        string
	Description string
	Hidden      bool
	Selected    func(i *Item)
}

type Menu struct {
	*tview.Box
	title string
	table *tview.Table
	items []*Item
}

func New(title string) *Menu {
	menu := &Menu{
		Box:   tview.NewBox(),
		title: title,
		items: make([]*Item, 0),
		table: tview.NewTable(),
	}

	menu.table.SetBorders(false)
	menu.table.SetSelectable(true, false)
	menu.table.SetTitle(fmt.Sprintf("[::b]%s", menu.title))
	menu.table.SetBorderColor(style.BorderColor)
	menu.table.SetBackgroundColor(style.BgColor)
	menu.table.SetTitleColor(style.FgColor)
	menu.table.SetFixed(1, 0)
	menu.table.Select(1, 0).SetSelectedFunc(func(row, column int) {
		if row == 0 {
			return // 忽略表头
		}

		item, ok := menu.table.GetCell(row, 0).GetReference().(*Item)
		if ok {
			if item.Selected != nil {
				item.Selected(item)
			}
		}
	})

	menu.setTableHeader()

	return menu
}

func (m *Menu) setTableHeader() {
	m.table.SetCell(0, 0, tview.NewTableCell(fmt.Sprintf("[black::b]%s", "命令")).
		SetExpansion(1).
		SetBackgroundColor(style.PageHeaderBgColor).
		SetTextColor(style.PageHeaderFgColor).
		SetAlign(tview.AlignLeft).
		SetSelectable(false))

	m.table.SetCell(0, 1, tview.NewTableCell(fmt.Sprintf("[black::b]%s", "说明")).
		SetExpansion(2).
		SetBackgroundColor(style.PageHeaderBgColor).
		SetTextColor(style.PageHeaderFgColor).
		SetAlign(tview.AlignLeft).
		SetSelectable(false))
}

func (m *Menu) AddItem(item *Item) {
	m.items = append(m.items, item)
	sort.Sort(SortItems(m.items))
	m.refresh()
}

func (m *Menu) SetItems(items []*Item) {
	m.items = items
	m.refresh()
}

func (m *Menu) GetItems() []*Item {
	return m.items
}

func (m *Menu) refresh() {
	m.table.Clear()
	m.setTableHeader()

	row := 1
	for _, item := range m.items {
		if item.Hidden {
			continue
		}
		m.table.SetCell(row, 0, tview.NewTableCell(item.Name).
			SetTextColor(style.FgColor).
			SetBackgroundColor(style.BgColor).
			SetReference(item).
			SetAlign(tview.AlignLeft))
		m.table.SetCell(row, 1, tview.NewTableCell(item.Description).
			SetTextColor(style.FgColor).
			SetBackgroundColor(style.BgColor).
			SetReference(item).
			SetAlign(tview.AlignLeft))
		row++
	}

}

func (m *Menu) Draw(screen tcell.Screen) {
	m.refresh()

	m.Box.DrawForSubclass(screen, m)
	m.Box.SetBorder(false)

	menuViewX, menuViewY, menuViewW, menuViewH := m.GetInnerRect()

	m.table.SetRect(menuViewX, menuViewY, menuViewW, menuViewH)
	m.table.SetBorder(true).SetBorderColor(style.BorderColor)

	m.table.Draw(screen)
}

func (m *Menu) Focus(delegate func(p tview.Primitive)) {
	delegate(m.table)
}

// HasFocus returns whether or not this primitive has focus
func (m *Menu) HasFocus() bool {
	// Check if the active menu has focus
	return m.table.HasFocus()
}

func (m *Menu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
	return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
		// 将事件传递给表格
		if handler := m.table.InputHandler(); handler != nil {
			handler(event, setFocus)
		}
	})
}

type SortItems []*Item

func (l SortItems) Len() int {
	return len(l)
}

func (l SortItems) Less(i, j int) bool {
	return l[i].Index < l[j].Index
}

func (l SortItems) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

package conf

import "github.com/sjzar/chatlog/pkg/config"

type Config struct {
	ConfigDir   string          `mapstructure:"-"`
	LastAccount string          `mapstructure:"last_account" json:"last_account"`
	History     []ProcessConfig `mapstructure:"history" json:"history"`
}

type ProcessConfig struct {
	Type        string `mapstructure:"type" json:"type"`
	Account     string `mapstructure:"account" json:"account"`
	Platform    string `mapstructure:"platform" json:"platform"`
	Version     int    `mapstructure:"version" json:"version"`
	FullVersion string `mapstructure:"full_version" json:"full_version"`
	DataDir     string `mapstructure:"data_dir" json:"data_dir"`
	DataKey     string `mapstructure:"data_key" json:"data_key"`
	WorkDir     string `mapstructure:"work_dir" json:"work_dir"`
	HTTPEnabled bool   `mapstructure:"http_enabled" json:"http_enabled"`
	HTTPAddr    string `mapstructure:"http_addr" json:"http_addr"`
	LastTime    int64  `mapstructure:"last_time" json:"last_time"`
	Files       []File `mapstructure:"files" json:"files"`
}

type File struct {
	Path         string `mapstructure:"path" json:"path"`
	ModifiedTime int64  `mapstructure:"modified_time" json:"modified_time"`
	Size         int64  `mapstructure:"size" json:"size"`
}

func (c *Config) ParseHistory() map[string]ProcessConfig {
	m := make(map[string]ProcessConfig)
	for _, v := range c.History {
		m[v.Account] = v
	}
	return m
}

func (c *Config) UpdateHistory(account string, conf ProcessConfig) error {
	if c.History == nil {
		c.History = make([]ProcessConfig, 0)
	}
	if len(c.History) == 0 {
		c.History = append(c.History, conf)
	} else {
		isFind := false
		for i, v := range c.History {
			if v.Account == account {
				isFind = true
				c.History[i] = conf
				break
			}
		}
		if !isFind {
			c.History = append(c.History, conf)
		}
	}
	config.SetConfig("last_account", account)
	return config.SetConfig("history", c.History)
}

package wechat

import (
	"context"
	"os"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt"
	"github.com/sjzar/chatlog/internal/wechat/key"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

// Account 表示一个微信账号
type Account struct {
	Name        string
	Platform    string
	Version     int
	FullVersion string
	DataDir     string
	Key         string
	PID         uint32
	ExePath     string
	Status      string
}

// NewAccount 创建新的账号对象
func NewAccount(proc *model.Process) *Account {
	return &Account{
		Name:        proc.AccountName,
		Platform:    proc.Platform,
		Version:     proc.Version,
		FullVersion: proc.FullVersion,
		DataDir:     proc.DataDir,
		PID:         proc.PID,
		ExePath:     proc.ExePath,
		Status:      proc.Status,
	}
}

// RefreshStatus 刷新账号的进程状态
func (a *Account) RefreshStatus() error {
	// 查找所有微信进程
	Load()

	process, err := GetProcess(a.Name)
	if err != nil {
		a.Status = model.StatusOffline
		return nil
	}

	if process.AccountName == a.Name {
		// 更新进程信息
		a.PID = process.PID
		a.ExePath = process.ExePath
		a.Platform = process.Platform
		a.Version = process.Version
		a.FullVersion = process.FullVersion
		a.Status = process.Status
		a.DataDir = process.DataDir
	}

	return nil
}

// GetKey 获取账号的密钥
func (a *Account) GetKey(ctx context.Context) (string, error) {
	// 如果已经有密钥，直接返回
	if a.Key != "" {
		return a.Key, nil
	}

	// 刷新进程状态
	if err := a.RefreshStatus(); err != nil {
		return "", errors.RefreshProcessStatusFailed(err)
	}

	// 检查账号状态
	if a.Status != model.StatusOnline {
		return "", errors.WeChatAccountNotOnline(a.Name)
	}

	// 创建密钥提取器 - 使用新的接口，传入平台和版本信息
	extractor, err := key.NewExtractor(a.Platform, a.Version)
	if err != nil {
		return "", err
	}

	process, err := GetProcess(a.Name)
	if err != nil {
		return "", err
	}

	validator, err := decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
	if err != nil {
		return "", err
	}

	extractor.SetValidate(validator)

	// 提取密钥
	key, err := extractor.Extract(ctx, process)
	if err != nil {
		return "", err
	}

	// 保存密钥
	a.Key = key
	return key, nil
}

// DecryptDatabase 解密数据库
func (a *Account) DecryptDatabase(ctx context.Context, dbPath, outputPath string) error {
	// 获取密钥
	hexKey, err := a.GetKey(ctx)
	if err != nil {
		return err
	}

	// 创建解密器 - 传入平台信息和版本
	decryptor, err := decrypt.NewDecryptor(a.Platform, a.Version)
	if err != nil {
		return err
	}

	// 创建输出文件
	output, err := os.Create(outputPath)
	if err != nil {
		return err
	}
	defer output.Close()

	// 解密数据库
	return decryptor.Decrypt(ctx, dbPath, hexKey, output)
}

package decrypt

import (
	"path/filepath"

	"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
)

type Validator struct {
	platform  string
	version   int
	dbPath    string
	decryptor Decryptor
	dbFile    *common.DBFile
}

// NewValidator 创建一个仅用于验证的验证器
func NewValidator(platform string, version int, dataDir string) (*Validator, error) {
	dbFile := GetSimpleDBFile(platform, version)
	dbPath := filepath.Join(dataDir + "/" + dbFile)
	return NewValidatorWithFile(platform, version, dbPath)
}

func NewValidatorWithFile(platform string, version int, dbPath string) (*Validator, error) {
	decryptor, err := NewDecryptor(platform, version)
	if err != nil {
		return nil, err
	}
	d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
	if err != nil {
		return nil, err
	}

	return &Validator{
		platform:  platform,
		version:   version,
		dbPath:    dbPath,
		decryptor: decryptor,
		dbFile:    d,
	}, nil
}

func (v *Validator) Validate(key []byte) bool {
	return v.decryptor.Validate(v.dbFile.FirstPage, key)
}

func GetSimpleDBFile(platform string, version int) string {
	switch {
	case platform == "windows" && version == 3:
		return "Msg\\Misc.db"
	case platform == "windows" && version == 4:
		return "db_storage\\message\\message_0.db"
	case platform == "darwin" && version == 3:
		return "Message/msg_0.db"
	case platform == "darwin" && version == 4:
		return "db_storage/message/message_0.db"
	}
	return ""

}

package mcp

// Document: https://modelcontextprotocol.io/docs/concepts/prompts

const (
	// Client => Server
	MethodPromptsList = "prompts/list"
	MethodPromptsGet  = "prompts/get"
)

// Prompt
//
//	{
//		name: string;              // Unique identifier for the prompt
//		description?: string;      // Human-readable description
//		arguments?: [              // Optional list of arguments
//			{
//				name: string;          // Argument identifier
//				description?: string;  // Argument description
//				required?: boolean;    // Whether argument is required
//			}
//		]
//	}
type Prompt struct {
	Name        string           `json:"name"`
	Description string           `json:"description,omitempty"`
	Arguments   []PromptArgument `json:"arguments,omitempty"`
}

type PromptArgument struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required,omitempty"`
}

// ListPrompts
//
//	{
//		prompts: [
//			{
//				name: "analyze-code",
//				description: "Analyze code for potential improvements",
//				arguments: [
//					{
//						name: "language",
//						description: "Programming language",
//						required: true
//					}
//				]
//			}
//		]
//	}
type PromptsListResponse struct {
	Prompts []Prompt `json:"prompts"`
}

// Use Prompt
// Request
//
//	{
//		method: "prompts/get",
//		params: {
//			name: "analyze-code",
//			arguments: {
//				language: "python"
//			}
//		}
//	}
//
// Response
//
//	{
//		description: "Analyze Python code for potential improvements",
//		messages: [
//			{
//				role: "user",
//				content: {
//					type: "text",
//					text: "Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n    total = 0\n    for num in numbers:\n        total = total + num\n    return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
//				}
//			}
//		]
//	}
type PromptsGetRequest struct {
	Name      string `json:"name"`
	Arguments M      `json:"arguments"`
}

type PromptsGetResponse struct {
	Description string          `json:"description"`
	Messages    []PromptMessage `json:"messages"`
}

type PromptMessage struct {
	Role    string        `json:"role"`
	Content PromptContent `json:"content"`
}

type PromptContent struct {
	Type     string      `json:"type"`
	Text     string      `json:"text,omitempty"`
	Resource interface{} `json:"resource,omitempty"` // Resource or ResourceTemplate
}

// {
// 	"messages": [
// 		{
// 			"role": "user",
// 			"content": {
// 				"type": "text",
// 				"text": "Analyze these system logs and the code file for any issues:"
// 			}
// 		},
// 		{
// 			"role": "user",
// 			"content": {
// 				"type": "resource",
// 				"resource": {
// 					"uri": "logs://recent?timeframe=1h",
// 					"text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
// 					"mimeType": "text/plain"
// 				}
// 			}
// 		},
// 		{
// 			"role": "user",
// 			"content": {
// 				"type": "resource",
// 				"resource": {
// 					"uri": "file:///path/to/code.py",
// 					"text": "def connect_to_service(timeout=30):\n    retries = 3\n    for attempt in range(retries):\n        try:\n            return establish_connection(timeout)\n        except TimeoutError:\n            if attempt == retries - 1:\n                raise\n            time.sleep(5)\n\ndef establish_connection(timeout):\n    # Connection implementation\n    pass",
// 					"mimeType": "text/x-python"
// 				}
// 			}
// 		}
// 	]
// }

package chatlog

import (
	"fmt"

	"github.com/sjzar/chatlog/internal/chatlog"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

func init() {
	rootCmd.AddCommand(keyCmd)
	keyCmd.Flags().IntVarP(&pid, "pid", "p", 0, "pid")
}

var pid int
var keyCmd = &cobra.Command{
	Use:   "key",
	Short: "key",
	Run: func(cmd *cobra.Command, args []string) {
		m, err := chatlog.New("")
		if err != nil {
			log.Err(err).Msg("failed to create chatlog instance")
			return
		}
		ret, err := m.CommandKey(pid)
		if err != nil {
			log.Err(err).Msg("failed to get key")
			return
		}
		fmt.Println(ret)
	},
}

package errors

import (
	"errors"
	"fmt"
	"net/http"
	"runtime"
	"strings"

	"github.com/gin-gonic/gin"
)

type Error struct {
	Message string   `json:"message"` // 错误消息
	Cause   error    `json:"-"`       // 原始错误
	Code    int      `json:"-"`       // HTTP Code
	Stack   []string `json:"-"`       // 错误堆栈
}

func (e *Error) Error() string {
	if e.Cause != nil {
		return fmt.Sprintf("%s: %v", e.Message, e.Cause)
	}
	return fmt.Sprintf("%s", e.Message)
}

func (e *Error) String() string {
	return e.Error()
}

func (e *Error) Unwrap() error {
	return e.Cause
}

func (e *Error) WithStack() *Error {
	const depth = 32
	var pcs [depth]uintptr
	n := runtime.Callers(2, pcs[:])
	frames := runtime.CallersFrames(pcs[:n])

	stack := make([]string, 0, n)
	for {
		frame, more := frames.Next()
		if !strings.Contains(frame.File, "runtime/") {
			stack = append(stack, fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function))
		}
		if !more {
			break
		}
	}

	e.Stack = stack
	return e
}

func New(cause error, code int, message string) *Error {
	return &Error{
		Message: message,
		Cause:   cause,
		Code:    code,
	}
}

func Newf(cause error, code int, format string, args ...interface{}) *Error {
	return &Error{
		Message: fmt.Sprintf(format, args...),
		Cause:   cause,
		Code:    code,
	}
}

func Wrap(err error, message string, code int) *Error {
	if err == nil {
		return nil
	}

	if appErr, ok := err.(*Error); ok {
		return &Error{
			Message: message,
			Cause:   appErr.Cause,
			Code:    appErr.Code,
			Stack:   appErr.Stack,
		}
	}

	return New(err, code, message)
}

func GetCode(err error) int {
	if err == nil {
		return http.StatusOK
	}

	var appErr *Error
	if errors.As(err, &appErr) {
		return appErr.Code
	}

	return http.StatusInternalServerError
}

func RootCause(err error) error {
	for err != nil {
		unwrapped := errors.Unwrap(err)
		if unwrapped == nil {
			return err
		}
		err = unwrapped
	}
	return err
}

func Err(c *gin.Context, err error) {
	if appErr, ok := err.(*Error); ok {
		c.JSON(appErr.Code, appErr.Error())
		return
	}

	c.JSON(http.StatusInternalServerError, err.Error())
}

package decrypt

import (
	"context"
	"io"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/darwin"
	"github.com/sjzar/chatlog/internal/wechat/decrypt/windows"
)

// Decryptor 定义数据库解密的接口
type Decryptor interface {
	// Decrypt 解密数据库
	Decrypt(ctx context.Context, dbfile string, key string, output io.Writer) error

	// Validate 验证密钥是否有效
	Validate(page1 []byte, key []byte) bool

	// GetPageSize 返回页面大小
	GetPageSize() int

	// GetReserve 返回保留字节数
	GetReserve() int

	// GetHMACSize 返回HMAC大小
	GetHMACSize() int

	// GetVersion 返回解密器版本
	GetVersion() string
}

// NewDecryptor 创建一个新的解密器
func NewDecryptor(platform string, version int) (Decryptor, error) {
	// 根据平台返回对应的实现
	switch {
	case platform == "windows" && version == 3:
		return windows.NewV3Decryptor(), nil
	case platform == "windows" && version == 4:
		return windows.NewV4Decryptor(), nil
	case platform == "darwin" && version == 3:
		return darwin.NewV3Decryptor(), nil
	case platform == "darwin" && version == 4:
		return darwin.NewV4Decryptor(), nil
	default:
		return nil, errors.PlatformUnsupported(platform, version)
	}
}

package wechat

import (
	"context"
	"runtime"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/wechat/model"
	"github.com/sjzar/chatlog/internal/wechat/process"
)

var DefaultManager *Manager

func init() {
	DefaultManager = NewManager()
	DefaultManager.Load()
}

func Load() error {
	return DefaultManager.Load()
}

func GetAccount(name string) (*Account, error) {
	return DefaultManager.GetAccount(name)
}

func GetProcess(name string) (*model.Process, error) {
	return DefaultManager.GetProcess(name)
}

func GetAccounts() []*Account {
	return DefaultManager.GetAccounts()
}

// Manager 微信管理器
type Manager struct {
	detector   process.Detector
	accounts   []*Account
	processMap map[string]*model.Process
}

// NewManager 创建新的微信管理器
func NewManager() *Manager {
	return &Manager{
		detector:   process.NewDetector(runtime.GOOS),
		accounts:   make([]*Account, 0),
		processMap: make(map[string]*model.Process),
	}
}

// Load 加载微信进程信息
func (m *Manager) Load() error {
	// 查找微信进程
	processes, err := m.detector.FindProcesses()
	if err != nil {
		return err
	}

	// 转换为账号信息
	accounts := make([]*Account, 0, len(processes))
	processMap := make(map[string]*model.Process, len(processes))

	for _, p := range processes {
		account := NewAccount(p)

		accounts = append(accounts, account)
		if account.Name != "" {
			processMap[account.Name] = p
		}
	}

	m.accounts = accounts
	m.processMap = processMap

	return nil
}

// GetAccount 获取指定名称的账号
func (m *Manager) GetAccount(name string) (*Account, error) {
	p, err := m.GetProcess(name)
	if err != nil {
		return nil, err
	}
	return NewAccount(p), nil
}

func (m *Manager) GetProcess(name string) (*model.Process, error) {
	p, ok := m.processMap[name]
	if !ok {
		return nil, errors.WeChatAccountNotFound(name)
	}
	return p, nil
}

// GetAccounts 获取所有账号
func (m *Manager) GetAccounts() []*Account {
	return m.accounts
}

// DecryptDatabase 便捷方法：通过账号名解密数据库
func (m *Manager) DecryptDatabase(ctx context.Context, accountName, dbPath, outputPath string) error {
	// 获取账号
	account, err := m.GetAccount(accountName)
	if err != nil {
		return err
	}

	// 使用账号解密数据库
	return account.DecryptDatabase(ctx, dbPath, outputPath)
}

package model

import (
	"strings"
	"time"
)

type Session struct {
	UserName string    `json:"userName"`
	NOrder   int       `json:"nOrder"`
	NickName string    `json:"nickName"`
	Content  string    `json:"content"`
	NTime    time.Time `json:"nTime"`
}

// CREATE TABLE Session(
// strUsrName TEXT  PRIMARY KEY,
// nOrder INT DEFAULT 0,
// nUnReadCount INTEGER DEFAULT 0,
// parentRef TEXT,
// Reserved0 INTEGER DEFAULT 0,
// Reserved1 TEXT,
// strNickName TEXT,
// nStatus INTEGER,
// nIsSend INTEGER,
// strContent TEXT,
// nMsgType	INTEGER,
// nMsgLocalID INTEGER,
// nMsgStatus INTEGER,
// nTime INTEGER,
// editContent TEXT,
// othersAtMe INT,
// Reserved2 INTEGER DEFAULT 0,
// Reserved3 TEXT,
// Reserved4 INTEGER DEFAULT 0,
// Reserved5 TEXT,
// bytesXml BLOB
// )
type SessionV3 struct {
	StrUsrName  string `json:"strUsrName"`
	NOrder      int    `json:"nOrder"`
	StrNickName string `json:"strNickName"`
	StrContent  string `json:"strContent"`
	NTime       int64  `json:"nTime"`

	// NUnReadCount int    `json:"nUnReadCount"`
	// ParentRef    string `json:"parentRef"`
	// Reserved0    int    `json:"Reserved0"`
	// Reserved1    string `json:"Reserved1"`
	// NStatus      int    `json:"nStatus"`
	// NIsSend      int    `json:"nIsSend"`
	// NMsgType     int    `json:"nMsgType"`
	// NMsgLocalID  int    `json:"nMsgLocalID"`
	// NMsgStatus   int    `json:"nMsgStatus"`
	// EditContent  string `json:"editContent"`
	// OthersAtMe   int    `json:"othersAtMe"`
	// Reserved2    int    `json:"Reserved2"`
	// Reserved3    string `json:"Reserved3"`
	// Reserved4    int    `json:"Reserved4"`
	// Reserved5    string `json:"Reserved5"`
	// BytesXml     string `json:"bytesXml"`
}

func (s *SessionV3) Wrap() *Session {
	return &Session{
		UserName: s.StrUsrName,
		NOrder:   s.NOrder,
		NickName: s.StrNickName,
		Content:  s.StrContent,
		NTime:    time.Unix(int64(s.NTime), 0),
	}
}

func (s *Session) PlainText(limit int) string {
	buf := strings.Builder{}
	buf.WriteString(s.NickName)
	buf.WriteString("(")
	buf.WriteString(s.UserName)
	buf.WriteString(") ")
	buf.WriteString(s.NTime.Format("2006-01-02 15:04:05"))
	buf.WriteString("\n")
	if limit > 0 {
		if len(s.Content) > limit {
			buf.WriteString(s.Content[:limit])
			buf.WriteString(" <...>")
		} else {
			buf.WriteString(s.Content)
		}
	}
	buf.WriteString("\n")
	return buf.String()
}

package mcp

import (
	"github.com/sjzar/chatlog/internal/mcp"
)

// MCPTools 和资源定义
var (
	InitializeResponse = mcp.InitializeResponse{
		ProtocolVersion: mcp.ProtocolVersion,
		Capabilities:    mcp.DefaultCapabilities,
		ServerInfo: mcp.ServerInfo{
			Name:    "chatlog",
			Version: "0.0.1",
		},
	}

	ToolContact = mcp.Tool{
		Name:        "query_contact",
		Description: "查询用户的联系人信息。可以通过姓名、备注名或ID进行查询，返回匹配的联系人列表。当用户询问某人的联系方式、想了解联系人信息或需要查找特定联系人时使用此工具。参数为空时，将返回联系人列表",
		InputSchema: mcp.ToolSchema{
			Type: "object",
			Properties: mcp.M{
				"keyword": mcp.M{
					"type":        "string",
					"description": "联系人的搜索关键词，可以是姓名、备注名或ID。",
				},
			},
			Required: []string{"keyword"},
		},
	}

	ToolChatRoom = mcp.Tool{
		Name:        "query_chat_room",
		Description: "查询用户参与的群聊信息。可以通过群名称、群ID或相关关键词进行查询，返回匹配的群聊列表。当用户询问群聊信息、想了解某个群的详情或需要查找特定群聊时使用此工具。",
		InputSchema: mcp.ToolSchema{
			Type: "object",
			Properties: mcp.M{
				"keyword": mcp.M{
					"type":        "string",
					"description": "群聊的搜索关键词，可以是群名称、群ID或相关描述",
				},
			},
			Required: []string{"keyword"},
		},
	}

	ToolRecentChat = mcp.Tool{
		Name:        "query_recent_chat",
		Description: "查询最近会话列表，包括个人聊天和群聊。当用户想了解最近的聊天记录、查看最近联系过的人或群组时使用此工具。不需要参数，直接返回最近的会话列表。",
		InputSchema: mcp.ToolSchema{
			Type:       "object",
			Properties: mcp.M{},
		},
	}

	ToolChatLog = mcp.Tool{
		Name: "chatlog",
		Description: `检索历史聊天记录，可根据时间、对话方、发送者和关键词等条件进行精确查询。当用户需要查找特定信息或想了解与某人/某群的历史交流时使用此工具。

【强制多步查询流程!】
当查询特定话题或特定发送者发言时，必须严格按照以下流程使用，任何偏离都会导致错误的结果：

步骤1: 初步定位相关消息
- 使用keyword参数查找特定话题
- 使用sender参数查找特定发送者的消息
- 使用较宽时间范围初步查询

步骤2: 【必须执行】针对每个关键结果点分别获取上下文
- 必须对步骤1返回的每个时间点T1, T2, T3...分别执行独立查询（时间范围接近的消息可以合并为一个查询）
- 每次独立查询必须移除keyword参数
- 每次独立查询必须移除sender参数
- 每次独立查询使用"Tn前后15-30分钟"的窄范围
- 每次独立查询仅保留talker参数

步骤3: 【必须执行】综合分析所有上下文
- 必须等待所有步骤2的查询结果返回后再进行分析
- 必须综合考虑所有上下文信息后再回答用户

【严格执行规则！】
- 禁止仅凭步骤1的结果直接回答用户
- 禁止在步骤2使用过大的时间范围一次性查询所有上下文
- 禁止跳过步骤2或步骤3
- 必须对每个关键结果点分别执行独立的上下文查询

【执行示例】
正确流程示例:
1. 步骤1: chatlog(time="2023-04-01~2023-04-30", talker="工作群", keyword="项目进度")
   返回结果: 4月5日、4月12日、4月20日有相关消息
2. 步骤2:
   - 查询1: chatlog(time="2023-04-05/09:30~2023-04-05/10:30", talker="工作群") // 注意没有keyword
   - 查询2: chatlog(time="2023-04-12/14:00~2023-04-12/15:00", talker="工作群") // 注意没有keyword
   - 查询3: chatlog(time="2023-04-20/16:00~2023-04-20/17:00", talker="工作群") // 注意没有keyword
3. 步骤3: 综合分析所有上下文后回答用户

错误流程示例:
- 仅执行步骤1后直接回答
- 步骤2使用time="2023-04-01~2023-04-30"一次性查询
- 步骤2仍然保留keyword或sender参数

【自我检查】回答用户前必须自问:
- 我是否对每个关键时间点都执行了独立的上下文查询?
- 我是否在上下文查询中移除了keyword和sender参数?
- 我是否分析了所有上下文后再回答?
- 如果上述任一问题答案为"否"，则必须纠正流程

返回格式："昵称(ID) 时间\n消息内容\n昵称(ID) 时间\n消息内容"
当查询多个Talker时，返回格式为："昵称(ID)\n[TalkerName(Talker)] 时间\n消息内容"

重要提示：
1. 当用户询问特定时间段内的聊天记录时，必须使用正确的时间格式，特别是包含小时和分钟的查询
2. 对于"今天下午4点到5点聊了啥"这类查询，正确的时间参数格式应为"2023-04-18/16:00~2023-04-18/17:00"
3. 当用户询问具体群聊中某人的聊天记录时，使用"sender"参数
4. 当用户询问包含特定关键词的聊天记录时，使用"keyword"参数`,
		InputSchema: mcp.ToolSchema{
			Type: "object",
			Properties: mcp.M{
				"time": mcp.M{
					"type": "string",
					"description": `指定查询的时间点或时间范围，格式必须严格遵循以下规则：

【单一时间点格式】
- 精确到日："2023-04-18"或"20230418"
- 精确到分钟（必须包含斜杠和冒号）："2023-04-18/14:30"或"20230418/14:30"（表示2023年4月18日14点30分）

【时间范围格式】（使用"~"分隔起止时间）
- 日期范围："2023-04-01~2023-04-18"
- 同一天的时间段："2023-04-18/14:30~2023-04-18/15:45"
  * 表示2023年4月18日14点30分到15点45分之间

【重要提示】包含小时分钟的格式必须使用斜杠和冒号："/"和":"
正确示例："2023-04-18/16:30"（4月18日下午4点30分）
错误示例："2023-04-18 16:30"、"2023-04-18T16:30"

【其他支持的格式】
- 年份："2023"
- 月份："2023-04"或"202304"`,
				},
				"talker": mcp.M{
					"type": "string",
					"description": `指定对话方（联系人或群组）
- 可使用ID、昵称或备注名
- 多个对话方用","分隔，如："张三,李四,工作群"
- 【重要】这是多步查询中唯一应保留的参数`,
				},
				"sender": mcp.M{
					"type": "string",
					"description": `指定群聊中的发送者
- 仅在查询群聊记录时有效
- 多个发送者用","分隔，如："张三,李四"
- 可使用ID、昵称或备注名
【重要】查询特定发送者的消息时：
  1. 第一步：使用sender参数初步定位多个相关消息时间点
  2. 后续步骤：必须移除sender参数，分别查询每个时间点前后的完整对话
  3. 错误示例：对所有找到的消息一次性查询大范围上下文
  4. 正确示例：对每个时间点T分别执行查询"T前后15-30分钟"（不带sender）`,
				},
				"keyword": mcp.M{
					"type": "string",
					"description": `搜索内容中的关键词
- 支持正则表达式匹配
- 【重要】查询特定话题时：
  1. 第一步：使用keyword参数初步定位多个相关消息时间点
  2. 后续步骤：必须移除keyword参数，分别查询每个时间点前后的完整对话
  3. 错误示例：对所有找到的关键词消息一次性查询大范围上下文
  4. 正确示例：对每个时间点T分别执行查询"T前后15-30分钟"（不带keyword）`,
				},
			},
			Required: []string{"time", "talker"},
		},
	}

	ToolCurrentTime = mcp.Tool{
		Name: "current_time",
		Description: `获取当前系统时间，返回RFC3339格式的时间字符串（包含用户本地时区信息）。
使用场景：
- 当用户询问"总结今日聊天记录"、"本周都聊了啥"等当前时间问题
- 当用户提及"昨天"、"上周"、"本月"等相对时间概念，需要确定基准时间点
- 需要执行依赖当前时间的计算（如"上个月5号我们有开会吗"）
返回示例：2025-04-18T21:29:00+08:00
注意：此工具不需要任何输入参数，直接调用即可获取当前时间。`,
		InputSchema: mcp.ToolSchema{
			Type:       "object",
			Properties: mcp.M{},
		},
	}

	ResourceRecentChat = mcp.Resource{
		Name:        "最近会话",
		URI:         "session://recent",
		Description: "获取最近的聊天会话列表",
	}

	ResourceTemplateContact = mcp.ResourceTemplate{
		Name:        "联系人信息",
		URITemplate: "contact://{username}",
		Description: "获取指定联系人的详细信息",
	}

	ResourceTemplateChatRoom = mcp.ResourceTemplate{
		Name:        "群聊信息",
		URITemplate: "chatroom://{roomid}",
		Description: "获取指定群聊的详细信息",
	}

	ResourceTemplateChatlog = mcp.ResourceTemplate{
		Name:        "聊天记录",
		URITemplate: "chatlog://{talker}/{timeframe}?limit,offset",
		Description: "获取与特定联系人或群聊的聊天记录",
	}
)

package windows

import (
	"context"

	"github.com/sjzar/chatlog/internal/wechat/decrypt"
)

type V4Extractor struct {
	validator *decrypt.Validator
}

func NewV4Extractor() *V4Extractor {
	return &V4Extractor{}
}

func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
	// TODO : Implement the key search logic for V4
	return "", false
}

func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
	e.validator = validator
}

package model

import (
	"strings"
	"time"
)

// CREATE TABLE Chat_md5(talker)(
// mesLocalID INTEGER PRIMARY KEY AUTOINCREMENT,
// mesSvrID INTEGER,msgCreateTime INTEGER,
// msgContent TEXT,msgStatus INTEGER,
// msgImgStatus INTEGER,
// messageType INTEGER,
// mesDes INTEGER,
// msgSource TEXT,
// IntRes1 INTEGER,
// IntRes2 INTEGER,
// StrRes1 TEXT,
// StrRes2 TEXT,
// msgVoiceText TEXT,
// msgSeq INTEGER,
// CompressContent BLOB,
// ConBlob BLOB
// )
type MessageDarwinV3 struct {
	MsgCreateTime int64  `json:"msgCreateTime"`
	MsgContent    string `json:"msgContent"`
	MessageType   int64  `json:"messageType"`
	MesDes        int    `json:"mesDes"` // 0: 发送, 1: 接收
}

func (m *MessageDarwinV3) Wrap(talker string) *Message {

	_m := &Message{
		Time:       time.Unix(m.MsgCreateTime, 0),
		Type:       m.MessageType,
		Talker:     talker,
		IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
		IsSelf:     m.MesDes == 0,
		Version:    WeChatDarwinV3,
	}

	content := m.MsgContent
	if _m.IsChatRoom {
		split := strings.SplitN(content, ":\n", 2)
		if len(split) == 2 {
			_m.Sender = split[0]
			content = split[1]
		}
	} else if !_m.IsSelf {
		_m.Sender = talker
	}

	_m.ParseMediaInfo(content)

	return _m
}

package mcp

import (
	"fmt"
)

// enum ErrorCode {
// 	// Standard JSON-RPC error codes
// 	ParseError = -32700,
// 	InvalidRequest = -32600,
// 	MethodNotFound = -32601,
// 	InvalidParams = -32602,
// 	InternalError = -32603
// }

// Error
type Error struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

var (
	ErrParseError     = &Error{Code: -32700, Message: "Parse error"}
	ErrInvalidRequest = &Error{Code: -32600, Message: "Invalid Request"}
	ErrMethodNotFound = &Error{Code: -32601, Message: "Method not found"}
	ErrInvalidParams  = &Error{Code: -32602, Message: "Invalid params"}
	ErrInternalError  = &Error{Code: -32603, Message: "Internal error"}

	ErrInvalidSessionID = &Error{Code: 400, Message: "Invalid session ID"}
	ErrSessionNotFound  = &Error{Code: 404, Message: "Could not find session"}
	ErrTooManyRequests  = &Error{Code: 429, Message: "Too many requests"}
)

func (e *Error) Error() string {
	return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

func (e *Error) JsonRPC() Response {
	return Response{
		JsonRPC: JsonRPCVersion,
		Error:   e,
	}
}

func NewErrorResponse(id interface{}, code int, err error) *Response {
	return &Response{
		JsonRPC: JsonRPCVersion,
		ID:      id,
		Error: &Error{
			Code:    code,
			Message: err.Error(),
		},
	}
}

package chatlog

import (
	"github.com/sjzar/chatlog/internal/chatlog"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

func init() {
	// windows only
	cobra.MousetrapHelpText = ""

	rootCmd.PersistentFlags().BoolVar(&Debug, "debug", false, "debug")
	rootCmd.PersistentPreRun = initLog
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		log.Err(err).Msg("command execution failed")
	}
}

var rootCmd = &cobra.Command{
	Use:     "chatlog",
	Short:   "chatlog",
	Long:    `chatlog`,
	Example: `chatlog`,
	Args:    cobra.MinimumNArgs(0),
	CompletionOptions: cobra.CompletionOptions{
		HiddenDefaultCmd: true,
	},
	PreRun: initTuiLog,
	Run:    Root,
}

func Root(cmd *cobra.Command, args []string) {

	m, err := chatlog.New("")
	if err != nil {
		log.Err(err).Msg("failed to create chatlog instance")
		return
	}

	if err := m.Run(); err != nil {
		log.Err(err).Msg("failed to run chatlog instance")
	}
}

package mcp

// Document: https://modelcontextprotocol.io/docs/concepts/tools

const (
	// Client => Server
	MethodToolsList = "tools/list"
	MethodToolsCall = "tools/call"
)

type M map[string]interface{}

// Tool
//
//	{
//		name: string;          // Unique identifier for the tool
//		description?: string;  // Human-readable description
//		inputSchema: {         // JSON Schema for the tool's parameters
//			type: "object",
//			properties: { ... }  // Tool-specific parameters
//		}
//	}
//
//	{
//		name: "analyze_csv",
//		description: "Analyze a CSV file",
//		inputSchema: {
//			type: "object",
//			properties: {
//				filepath: { type: "string" },
//				operations: {
//					type: "array",
//					items: {
//						enum: ["sum", "average", "count"]
//					}
//				}
//			}
//		}
//	}
//
//	{
//		"jsonrpc": "2.0",
//		"id": 1,
//		"result": {
//		  "tools": [
//			{
//			  "name": "get_alerts",
//			  "description": "Get weather alerts for a US state.\n\n    Args:\n        state: Two-letter US state code (e.g. CA, NY)\n    ",
//			  "inputSchema": {
//				"properties": {
//				  "state": {
//					"title": "State",
//					"type": "string"
//				  }
//				},
//				"required": [
//				  "state"
//				],
//				"title": "get_alertsArguments",
//				"type": "object"
//			  }
//			},
//			{
//			  "name": "get_forecast",
//			  "description": "Get weather forecast for a location.\n\n    Args:\n        latitude: Latitude of the location\n        longitude: Longitude of the location\n    ",
//			  "inputSchema": {
//				"properties": {
//				  "latitude": {
//					"title": "Latitude",
//					"type": "number"
//				  },
//				  "longitude": {
//					"title": "Longitude",
//					"type": "number"
//				  }
//				},
//				"required": [
//				  "latitude",
//				  "longitude"
//				],
//				"title": "get_forecastArguments",
//				"type": "object"
//			  }
//			}
//		  ]
//		}
//	  }
type Tool struct {
	Name        string     `json:"name"`
	Description string     `json:"description,omitempty"`
	InputSchema ToolSchema `json:"inputSchema"`
}

type ToolSchema struct {
	Type       string   `json:"type"`
	Properties M        `json:"properties"`
	Required   []string `json:"required,omitempty"`
}

//	{
//		"method": "tools/call",
//		"params": {
//		  "name": "chatlog",
//		  "arguments": {
//			"start": "2006-11-12",
//			"end": "2020-11-20",
//			"limit": "50",
//			"offset": "6"
//		  },
//		  "_meta": {
//			"progressToken": 1
//		  }
//		},
//		"jsonrpc": "2.0",
//		"id": 3
//	  }
type ToolsCallRequest struct {
	Name      string `json:"name"`
	Arguments M      `json:"arguments"`
}

//	{
//		"jsonrpc": "2.0",
//		"id": 2,
//		"result": {
//		  "content": [
//			{
//			  "type": "text",
//			  "text": "\nEvent: Winter Storm Warning\n"
//			}
//		  ],
//		  "isError": false
//		}
//	  }
type ToolsCallResponse struct {
	Content []Content `json:"content"`
	IsError bool      `json:"isError"`
}

type Content struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

package chatlog

import (
	"fmt"
	"runtime"

	"github.com/sjzar/chatlog/internal/chatlog"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

func init() {
	rootCmd.AddCommand(decryptCmd)
	decryptCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "data dir")
	decryptCmd.Flags().StringVarP(&workDir, "work-dir", "w", "", "work dir")
	decryptCmd.Flags().StringVarP(&key, "key", "k", "", "key")
	decryptCmd.Flags().StringVarP(&decryptPlatform, "platform", "p", runtime.GOOS, "platform")
	decryptCmd.Flags().IntVarP(&decryptVer, "version", "v", 3, "version")
}

var (
	dataDir         string
	workDir         string
	key             string
	decryptPlatform string
	decryptVer      int
)

var decryptCmd = &cobra.Command{
	Use:   "decrypt",
	Short: "decrypt",
	Run: func(cmd *cobra.Command, args []string) {
		m, err := chatlog.New("")
		if err != nil {
			log.Err(err).Msg("failed to create chatlog instance")
			return
		}
		if err := m.CommandDecrypt(dataDir, workDir, key, decryptPlatform, decryptVer); err != nil {
			log.Err(err).Msg("failed to decrypt")
			return
		}
		fmt.Println("decrypt success")
	},
}

package windows

import (
	"context"

	"github.com/sjzar/chatlog/internal/wechat/decrypt"
)

type V3Extractor struct {
	validator *decrypt.Validator
}

func NewV3Extractor() *V3Extractor {
	return &V3Extractor{}
}

func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
	// TODO : Implement the key search logic for V3
	return "", false
}

func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
	e.validator = validator
}

package errors

import "net/http"

var (
	ErrAlreadyDecrypted              = New(nil, http.StatusBadRequest, "database file is already decrypted")
	ErrDecryptHashVerificationFailed = New(nil, http.StatusBadRequest, "hash verification failed during decryption")
	ErrDecryptIncorrectKey           = New(nil, http.StatusBadRequest, "incorrect decryption key")
	ErrDecryptOperationCanceled      = New(nil, http.StatusBadRequest, "decryption operation was canceled")
	ErrNoMemoryRegionsFound          = New(nil, http.StatusBadRequest, "no memory regions found")
	ErrReadMemoryTimeout             = New(nil, http.StatusInternalServerError, "read memory timeout")
	ErrWeChatOffline                 = New(nil, http.StatusBadRequest, "WeChat is offline")
	ErrSIPEnabled                    = New(nil, http.StatusBadRequest, "SIP is enabled")
	ErrValidatorNotSet               = New(nil, http.StatusBadRequest, "validator not set")
	ErrNoValidKey                    = New(nil, http.StatusBadRequest, "no valid key found")
	ErrWeChatDLLNotFound             = New(nil, http.StatusBadRequest, "WeChatWin.dll module not found")
)

func PlatformUnsupported(platform string, version int) *Error {
	return Newf(nil, http.StatusBadRequest, "unsupported platform: %s v%d", platform, version).WithStack()
}

func DecryptCreateCipherFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to create cipher").WithStack()
}

func DecodeKeyFailed(cause error) *Error {
	return New(cause, http.StatusBadRequest, "failed to decode hex key").WithStack()
}

func CreatePipeFileFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to create pipe file").WithStack()
}

func OpenPipeFileFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to open pipe file").WithStack()
}

func ReadPipeFileFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to read from pipe file").WithStack()
}

func RunCmdFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to run command").WithStack()
}

func ReadMemoryFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to read memory").WithStack()
}

func OpenProcessFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to open process").WithStack()
}

func WeChatAccountNotFound(name string) *Error {
	return Newf(nil, http.StatusBadRequest, "WeChat account not found: %s", name).WithStack()
}

func WeChatAccountNotOnline(name string) *Error {
	return Newf(nil, http.StatusBadRequest, "WeChat account is not online: %s", name).WithStack()
}

func RefreshProcessStatusFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to refresh process status").WithStack()
}

package datasource

import (
	"context"
	"time"

	"github.com/fsnotify/fsnotify"

	"github.com/sjzar/chatlog/internal/errors"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource/darwinv3"
	v4 "github.com/sjzar/chatlog/internal/wechatdb/datasource/v4"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource/windowsv3"
)

type DataSource interface {

	// 消息
	GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error)

	// 联系人
	GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error)

	// 群聊
	GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error)

	// 最近会话
	GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error)

	// 媒体
	GetMedia(ctx context.Context, _type string, key string) (*model.Media, error)

	// 设置回调函数
	SetCallback(name string, callback func(event fsnotify.Event) error) error

	Close() error
}

func New(path string, platform string, version int) (DataSource, error) {
	switch {
	case platform == "windows" && version == 3:
		return windowsv3.New(path)
	case platform == "windows" && version == 4:
		return v4.New(path)
	case platform == "darwin" && version == 3:
		return darwinv3.New(path)
	case platform == "darwin" && version == 4:
		return v4.New(path)
	default:
		return nil, errors.PlatformUnsupported(platform, version)
	}
}

package infobar

import (
	"fmt"

	"github.com/sjzar/chatlog/internal/ui/style"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

const (
	Title = "infobar"
)

// InfoBarViewHeight info bar height.
const (
	InfoBarViewHeight = 7
	accountRow        = 0
	statusRow         = 1
	platformRow       = 2
	sessionRow        = 3
	dataUsageRow      = 4
	workUsageRow      = 5
	httpServerRow     = 6

	// 列索引
	labelCol1 = 0 // 第一列标签
	valueCol1 = 1 // 第一列值
	labelCol2 = 2 // 第二列标签
	valueCol2 = 3 // 第二列值
	totalCols = 4
)

// InfoBar implements the info bar primitive.
type InfoBar struct {
	*tview.Box
	title string
	table *tview.Table
}

// NewInfoBar returns info bar view.
func New() *InfoBar {
	table := tview.NewTable()
	headerColor := style.InfoBarItemFgColor

	// Account 和 PID 行
	table.SetCell(
		accountRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Account:")),
	)
	table.SetCell(accountRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		accountRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "PID:")),
	)
	table.SetCell(accountRow, valueCol2, tview.NewTableCell(""))

	// Status 和 ExePath 行
	table.SetCell(
		statusRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Status:")),
	)
	table.SetCell(statusRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		statusRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "ExePath:")),
	)
	table.SetCell(statusRow, valueCol2, tview.NewTableCell(""))

	// Platform 和 Version 行
	table.SetCell(
		platformRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Platform:")),
	)
	table.SetCell(platformRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		platformRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Version:")),
	)
	table.SetCell(platformRow, valueCol2, tview.NewTableCell(""))

	// Session 和 Data Key 行
	table.SetCell(
		sessionRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Session:")),
	)
	table.SetCell(sessionRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		sessionRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Key:")),
	)
	table.SetCell(sessionRow, valueCol2, tview.NewTableCell(""))

	// Data Usage 和 Data Dir 行
	table.SetCell(
		dataUsageRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Usage:")),
	)
	table.SetCell(dataUsageRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		dataUsageRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Data Dir:")),
	)
	table.SetCell(dataUsageRow, valueCol2, tview.NewTableCell(""))

	// Work Usage 和 Work Dir 行
	table.SetCell(
		workUsageRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Work Usage:")),
	)
	table.SetCell(workUsageRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		workUsageRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Work Dir:")),
	)
	table.SetCell(workUsageRow, valueCol2, tview.NewTableCell(""))

	// HTTP Server 行
	table.SetCell(
		httpServerRow,
		labelCol1,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "HTTP Server:")),
	)
	table.SetCell(httpServerRow, valueCol1, tview.NewTableCell(""))

	table.SetCell(
		httpServerRow,
		labelCol2,
		tview.NewTableCell(fmt.Sprintf(" [%s::]%s", headerColor, "Auto Decrypt:")),
	)
	table.SetCell(httpServerRow, valueCol2, tview.NewTableCell(""))

	// infobar
	infoBar := &InfoBar{
		Box:   tview.NewBox(),
		title: Title,
		table: table,
	}

	return infoBar
}

func (info *InfoBar) UpdateAccount(account string) {
	info.table.GetCell(accountRow, valueCol1).SetText(account)
}

func (info *InfoBar) UpdateBasicInfo(pid int, version string, exePath string) {
	info.table.GetCell(accountRow, valueCol2).SetText(fmt.Sprintf("%d", pid))
	info.table.GetCell(statusRow, valueCol2).SetText(exePath)
	info.table.GetCell(platformRow, valueCol2).SetText(version)
}

func (info *InfoBar) UpdateStatus(status string) {
	info.table.GetCell(statusRow, valueCol1).SetText(status)
}

func (info *InfoBar) UpdatePlatform(text string) {
	info.table.GetCell(platformRow, valueCol1).SetText(text)
}

func (info *InfoBar) UpdateSession(text string) {
	info.table.GetCell(sessionRow, valueCol1).SetText(text)
}

func (info *InfoBar) UpdateDataKey(key string) {
	info.table.GetCell(sessionRow, valueCol2).SetText(key)
}

func (info *InfoBar) UpdateDataUsageDir(dataUsage string, dataDir string) {
	info.table.GetCell(dataUsageRow, valueCol1).SetText(dataUsage)
	info.table.GetCell(dataUsageRow, valueCol2).SetText(dataDir)
}

func (info *InfoBar) UpdateWorkUsageDir(workUsage string, workDir string) {
	info.table.GetCell(workUsageRow, valueCol1).SetText(workUsage)
	info.table.GetCell(workUsageRow, valueCol2).SetText(workDir)
}

// UpdateHTTPServer updates HTTP Server value.
func (info *InfoBar) UpdateHTTPServer(server string) {
	info.table.GetCell(httpServerRow, valueCol1).SetText(server)
}

// UpdateAutoDecrypt updates Auto Decrypt value.
func (info *InfoBar) UpdateAutoDecrypt(text string) {
	info.table.GetCell(httpServerRow, valueCol2).SetText(text)
}

// Draw draws this primitive onto the screen.
func (info *InfoBar) Draw(screen tcell.Screen) {
	info.Box.DrawForSubclass(screen, info)
	info.Box.SetBorder(false)

	x, y, width, height := info.GetInnerRect()

	info.table.SetRect(x, y, width, height)
	info.table.SetBorder(false)
	info.table.Draw(screen)
}

package model

// CREATE TABLE chat_room(
// id INTEGER PRIMARY KEY,
// username TEXT,
// owner TEXT,
// ext_buffer BLOB
// )
type ChatRoomV4 struct {
	ID        int    `json:"id"`
	UserName  string `json:"username"`
	Owner     string `json:"owner"`
	ExtBuffer []byte `json:"ext_buffer"`
}

func (c *ChatRoomV4) Wrap() *ChatRoom {

	var users []ChatRoomUser
	if len(c.ExtBuffer) != 0 {
		users = ParseRoomData(c.ExtBuffer)
	}

	user2DisplayName := make(map[string]string, len(users))
	for _, user := range users {
		if user.DisplayName != "" {
			user2DisplayName[user.UserName] = user.DisplayName
		}
	}

	return &ChatRoom{
		Name:             c.UserName,
		Owner:            c.Owner,
		Users:            users,
		User2DisplayName: user2DisplayName,
	}
}

package model

import "strings"

// CREATE TABLE GroupContact(
// m_nsUsrName TEXT PRIMARY KEY ASC,
// m_uiConType INTEGER,
// nickname TEXT,
// m_nsFullPY TEXT,
// m_nsShortPY TEXT,
// m_nsRemark TEXT,
// m_nsRemarkPYFull TEXT,
// m_nsRemarkPYShort TEXT,
// m_uiCertificationFlag INTEGER,
// m_uiSex INTEGER,
// m_uiType INTEGER,
// m_nsImgStatus TEXT,
// m_uiImgKey INTEGER,
// m_nsHeadImgUrl TEXT,
// m_nsHeadHDImgUrl TEXT,
// m_nsHeadHDMd5 TEXT,
// m_nsChatRoomMemList TEXT,
// m_nsChatRoomAdminList TEXT,
// m_uiChatRoomStatus INTEGER,
// m_nsChatRoomDesc TEXT,
// m_nsDraft TEXT,
// m_nsBrandIconUrl TEXT,
// m_nsGoogleContactName TEXT,
// m_nsAliasName TEXT,
// m_nsEncodeUserName TEXT,
// m_uiChatRoomVersion INTEGER,
// m_uiChatRoomMaxCount INTEGER,
// m_uiChatRoomType INTEGER,
// m_patSuffix TEXT,
// richChatRoomDesc TEXT,
// _packed_WCContactData BLOB,
// openIMInfo BLOB
// )
type ChatRoomDarwinV3 struct {
	M_nsUsrName           string `json:"m_nsUsrName"`
	Nickname              string `json:"nickname"`
	M_nsRemark            string `json:"m_nsRemark"`
	M_nsChatRoomMemList   string `json:"m_nsChatRoomMemList"`
	M_nsChatRoomAdminList string `json:"m_nsChatRoomAdminList"`

	// M_uiConType           int    `json:"m_uiConType"`
	// M_nsFullPY            string `json:"m_nsFullPY"`
	// M_nsShortPY           string `json:"m_nsShortPY"`
	// M_nsRemarkPYFull      string `json:"m_nsRemarkPYFull"`
	// M_nsRemarkPYShort     string `json:"m_nsRemarkPYShort"`
	// M_uiCertificationFlag int    `json:"m_uiCertificationFlag"`
	// M_uiSex               int    `json:"m_uiSex"`
	// M_uiType              int    `json:"m_uiType"`
	// M_nsImgStatus         string `json:"m_nsImgStatus"`
	// M_uiImgKey            int    `json:"m_uiImgKey"`
	// M_nsHeadImgUrl        string `json:"m_nsHeadImgUrl"`
	// M_nsHeadHDImgUrl      string `json:"m_nsHeadHDImgUrl"`
	// M_nsHeadHDMd5         string `json:"m_nsHeadHDMd5"`
	// M_uiChatRoomStatus    int    `json:"m_uiChatRoomStatus"`
	// M_nsChatRoomDesc      string `json:"m_nsChatRoomDesc"`
	// M_nsDraft             string `json:"m_nsDraft"`
	// M_nsBrandIconUrl      string `json:"m_nsBrandIconUrl"`
	// M_nsGoogleContactName string `json:"m_nsGoogleContactName"`
	// M_nsAliasName         string `json:"m_nsAliasName"`
	// M_nsEncodeUserName    string `json:"m_nsEncodeUserName"`
	// M_uiChatRoomVersion   int    `json:"m_uiChatRoomVersion"`
	// M_uiChatRoomMaxCount  int    `json:"m_uiChatRoomMaxCount"`
	// M_uiChatRoomType      int    `json:"m_uiChatRoomType"`
	// M_patSuffix           string `json:"m_patSuffix"`
	// RichChatRoomDesc      string `json:"richChatRoomDesc"`
	// Packed_WCContactData  []byte `json:"_packed_WCContactData"`
	// OpenIMInfo            []byte `json:"openIMInfo"`
}

func (c *ChatRoomDarwinV3) Wrap(user2DisplayName map[string]string) *ChatRoom {

	split := strings.Split(c.M_nsChatRoomMemList, ";")
	users := make([]ChatRoomUser, 0, len(split))
	_user2DisplayName := make(map[string]string)
	for _, v := range split {
		users = append(users, ChatRoomUser{
			UserName: v,
		})
		if name, ok := user2DisplayName[v]; ok {
			_user2DisplayName[v] = name
		}
	}

	return &ChatRoom{
		Name:             c.M_nsUsrName,
		Owner:            c.M_nsChatRoomAdminList,
		Remark:           c.M_nsRemark,
		NickName:         c.Nickname,
		Users:            users,
		User2DisplayName: _user2DisplayName,
	}
}

package wechatdb

import (
	"context"
	"time"

	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb/datasource"
	"github.com/sjzar/chatlog/internal/wechatdb/repository"

	_ "github.com/mattn/go-sqlite3"
)

type DB struct {
	path     string
	platform string
	version  int
	ds       datasource.DataSource
	repo     *repository.Repository
}

func New(path string, platform string, version int) (*DB, error) {

	w := &DB{
		path:     path,
		platform: platform,
		version:  version,
	}

	// 初始化，加载数据库文件信息
	if err := w.Initialize(); err != nil {
		return nil, err
	}

	return w, nil
}

func (w *DB) Close() error {
	if w.repo != nil {
		return w.repo.Close()
	}
	return nil
}

func (w *DB) Initialize() error {
	var err error
	w.ds, err = datasource.New(w.path, w.platform, w.version)
	if err != nil {
		return err
	}

	w.repo, err = repository.New(w.ds)
	if err != nil {
		return err
	}

	return nil
}

func (w *DB) GetMessages(start, end time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
	ctx := context.Background()

	// 使用 repository 获取消息
	messages, err := w.repo.GetMessages(ctx, start, end, talker, sender, keyword, limit, offset)
	if err != nil {
		return nil, err
	}

	return messages, nil
}

type GetContactsResp struct {
	Items []*model.Contact `json:"items"`
}

func (w *DB) GetContacts(key string, limit, offset int) (*GetContactsResp, error) {
	ctx := context.Background()

	contacts, err := w.repo.GetContacts(ctx, key, limit, offset)
	if err != nil {
		return nil, err
	}

	return &GetContactsResp{
		Items: contacts,
	}, nil
}

type GetChatRoomsResp struct {
	Items []*model.ChatRoom `json:"items"`
}

func (w *DB) GetChatRooms(key string, limit, offset int) (*GetChatRoomsResp, error) {
	ctx := context.Background()

	chatRooms, err := w.repo.GetChatRooms(ctx, key, limit, offset)
	if err != nil {
		return nil, err
	}

	return &GetChatRoomsResp{
		Items: chatRooms,
	}, nil
}

type GetSessionsResp struct {
	Items []*model.Session `json:"items"`
}

func (w *DB) GetSessions(key string, limit, offset int) (*GetSessionsResp, error) {
	ctx := context.Background()

	// 使用 repository 获取会话列表
	sessions, err := w.repo.GetSessions(ctx, key, limit, offset)
	if err != nil {
		return nil, err
	}

	return &GetSessionsResp{
		Items: sessions,
	}, nil
}

func (w *DB) GetMedia(_type string, key string) (*model.Media, error) {
	return w.repo.GetMedia(context.Background(), _type, key)
}

package mcp

import (
	"encoding/json"
	"io"

	"github.com/gin-gonic/gin"
)

type Session struct {
	id string
	w  io.Writer
	c  *ClientInfo
}

func NewSession(c *gin.Context, id string) *Session {
	return &Session{
		id: id,
		w:  NewSSEWriter(c, id),
	}
}

func (s *Session) Write(p []byte) (n int, err error) {
	return s.w.Write(p)
}

func (s *Session) WriteError(req *Request, err error) {
	resp := NewErrorResponse(req.ID, 500, err)
	b, err := json.Marshal(resp)
	if err != nil {
		return
	}
	s.Write(b)
}

func (s *Session) WriteResponse(req *Request, data interface{}) error {
	resp := NewResponse(req.ID, data)
	b, err := json.Marshal(resp)
	if err != nil {
		return err
	}
	s.Write(b)
	return nil
}

func (s *Session) SaveClientInfo(c *ClientInfo) {
	s.c = c
}

package model

type Contact struct {
	UserName string `json:"userName"`
	Alias    string `json:"alias"`
	Remark   string `json:"remark"`
	NickName string `json:"nickName"`
	IsFriend bool   `json:"isFriend"`
}

// CREATE TABLE Contact(
// UserName TEXT PRIMARY KEY ,
// Alias TEXT,
// EncryptUserName TEXT,
// DelFlag INTEGER DEFAULT 0,
// Type INTEGER DEFAULT 0,
// VerifyFlag INTEGER DEFAULT 0,
// Reserved1 INTEGER DEFAULT 0,
// Reserved2 INTEGER DEFAULT 0,
// Reserved3 TEXT,
// Reserved4 TEXT,
// Remark TEXT,
// NickName TEXT,
// LabelIDList TEXT,
// DomainList TEXT,
// ChatRoomType int,
// PYInitial TEXT,
// QuanPin TEXT,
// RemarkPYInitial TEXT,
// RemarkQuanPin TEXT,
// BigHeadImgUrl TEXT,
// SmallHeadImgUrl TEXT,
// HeadImgMd5 TEXT,
// ChatRoomNotify INTEGER DEFAULT 0,
// Reserved5 INTEGER DEFAULT 0,
// Reserved6 TEXT,
// Reserved7 TEXT,
// ExtraBuf BLOB,
// Reserved8 INTEGER DEFAULT 0,
// Reserved9 INTEGER DEFAULT 0,
// Reserved10 TEXT,
// Reserved11 TEXT
// )
type ContactV3 struct {
	UserName  string `json:"UserName"`
	Alias     string `json:"Alias"`
	Remark    string `json:"Remark"`
	NickName  string `json:"NickName"`
	Reserved1 int    `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
}

func (c *ContactV3) Wrap() *Contact {
	return &Contact{
		UserName: c.UserName,
		Alias:    c.Alias,
		Remark:   c.Remark,
		NickName: c.NickName,
		IsFriend: c.Reserved1 == 1,
	}
}

func (c *Contact) DisplayName() string {
	switch {
	case c.Remark != "":
		return c.Remark
	case c.NickName != "":
		return c.NickName
	}
	return ""
}

package mcp

// Document: https://modelcontextprotocol.io/docs/concepts/resources

const (
	// Client => Server
	MethodResourcesList         = "resources/list"
	MethodResourcesTemplateList = "resources/templates/list"
	MethodResourcesRead         = "resources/read"
	MethodResourcesSubscribe    = "resources/subscribe"
	MethodResourcesUnsubscribe  = "resources/unsubscribe"

	// Server => Client
	NotificationResourcesListChanged = "notifications/resources/list_changed"
	NofiticationResourcesUpdated     = "notifications/resources/updated"
)

// Direct resources
//
//	{
//		uri: string;           // Unique identifier for the resource
//		name: string;          // Human-readable name
//		description?: string;  // Optional description
//		mimeType?: string;     // Optional MIME type
//	}
type Resource struct {
	URI         string `json:"uri"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	MimeType    string `json:"mimeType,omitempty"`
}

// Resource templates
//
//	{
//		uriTemplate: string;   // URI template following RFC 6570
//		name: string;          // Human-readable name for this type
//		description?: string;  // Optional description
//		mimeType?: string;     // Optional MIME type for all matching resources
//	}
type ResourceTemplate struct {
	URITemplate string `json:"uriTemplate"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	MimeType    string `json:"mimeType,omitempty"`
}

// Reading resources
// {
// 	contents: [
// 		{
// 			uri: string;        // The URI of the resource
// 			mimeType?: string;  // Optional MIME type

//				// One of:
//				text?: string;      // For text resources
//				blob?: string;      // For binary resources (base64 encoded)
//			}
//		]
//	}
type ReadingResource struct {
	Contents []ReadingResourceContent `json:"contents"`
}

type ResourcesReadRequest struct {
	URI string `json:"uri"`
}

type ReadingResourceContent struct {
	URI      string `json:"uri"`
	MimeType string `json:"mimeType,omitempty"`
	Text     string `json:"text,omitempty"`
	Blob     string `json:"blob,omitempty"`
}

package database

import (
	"time"

	"github.com/sjzar/chatlog/internal/chatlog/ctx"
	"github.com/sjzar/chatlog/internal/model"
	"github.com/sjzar/chatlog/internal/wechatdb"
)

type Service struct {
	ctx *ctx.Context
	db  *wechatdb.DB
}

func NewService(ctx *ctx.Context) *Service {
	return &Service{
		ctx: ctx,
	}
}

func (s *Service) Start() error {
	db, err := wechatdb.New(s.ctx.WorkDir, s.ctx.Platform, s.ctx.Version)
	if err != nil {
		return err
	}
	s.db = db
	return nil
}

func (s *Service) Stop() error {
	if s.db != nil {
		s.db.Close()
	}
	s.db = nil
	return nil
}

func (s *Service) GetDB() *wechatdb.DB {
	return s.db
}

func (s *Service) GetMessages(start, end time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
	return s.db.GetMessages(start, end, talker, sender, keyword, limit, offset)
}

func (s *Service) GetContacts(key string, limit, offset int) (*wechatdb.GetContactsResp, error) {
	return s.db.GetContacts(key, limit, offset)
}

func (s *Service) GetChatRooms(key string, limit, offset int) (*wechatdb.GetChatRoomsResp, error) {
	return s.db.GetChatRooms(key, limit, offset)
}

// GetSession retrieves session information
func (s *Service) GetSessions(key string, limit, offset int) (*wechatdb.GetSessionsResp, error) {
	return s.db.GetSessions(key, limit, offset)
}

func (s *Service) GetMedia(_type string, key string) (*model.Media, error) {
	return s.db.GetMedia(_type, key)
}

// Close closes the database connection
func (s *Service) Close() {
	// Add cleanup code if needed
	s.db.Close()
}

package mcp

const (
	MethodInitialize = "initialize"
	MethodPing       = "ping"
	ProtocolVersion  = "2024-11-05"
)

//	{
//		"method": "initialize",
//		"params": {
//		  "protocolVersion": "2024-11-05",
//		  "capabilities": {
//			"sampling": {},
//			"roots": {
//			  "listChanged": true
//			}
//		  },
//		  "clientInfo": {
//			"name": "mcp-inspector",
//			"version": "0.0.1"
//		  }
//		},
//		"jsonrpc": "2.0",
//		"id": 0
//	  }
type InitializeRequest struct {
	ProtocolVersion string      `json:"protocolVersion"`
	Capabilities    M           `json:"capabilities"`
	ClientInfo      *ClientInfo `json:"clientInfo"`
}

type ClientInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

//	{
//		"jsonrpc": "2.0",
//		"id": 0,
//		"result": {
//		  "protocolVersion": "2024-11-05",
//		  "capabilities": {
//			"experimental": {},
//			"prompts": {
//			  "listChanged": false
//			},
//			"resources": {
//			  "subscribe": false,
//			  "listChanged": false
//			},
//			"tools": {
//			  "listChanged": false
//			}
//		  },
//		  "serverInfo": {
//			"name": "weather",
//			"version": "1.4.1"
//		  }
//		}
//	  }
type InitializeResponse struct {
	ProtocolVersion string     `json:"protocolVersion"`
	Capabilities    M          `json:"capabilities"`
	ServerInfo      ServerInfo `json:"serverInfo"`
}

type ServerInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

var DefaultCapabilities = M{
	"experimental": M{},
	"prompts":      M{"listChanged": false},
	"resources":    M{"subscribe": false, "listChanged": false},
	"tools":        M{"listChanged": false},
}

//go:build windows

package style

import (
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

const (
	// HeavyGreenCheckMark unicode.
	HeavyGreenCheckMark = "[green::]\u25CF[-::]"
	// HeavyRedCrossMark unicode.
	HeavyRedCrossMark = "[red::]\u25CF[-::]"
	// ProgressBar cell.
	ProgressBarCell = "\u2593"
)

var (
	// infobar.
	InfoBarItemFgColor = tcell.ColorGray
	// main views.
	FgColor              = tview.Styles.PrimaryTextColor
	BgColor              = tview.Styles.PrimitiveBackgroundColor
	BorderColor          = tcell.ColorSpringGreen
	MenuBgColor          = tcell.ColorSpringGreen
	HelpHeaderFgColor    = tcell.ColorSpringGreen
	PageHeaderBgColor    = tcell.ColorSpringGreen
	PageHeaderFgColor    = tview.Styles.PrimaryTextColor
	RunningStatusFgColor = tcell.ColorLime
	PausedStatusFgColor  = tcell.ColorYellow

	// dialogs.
	DialogBgColor            = tview.Styles.PrimitiveBackgroundColor
	DialogFgColor            = tview.Styles.PrimaryTextColor
	DialogBorderColor        = tcell.ColorSpringGreen
	DialogSubBoxBorderColor  = tcell.ColorGray
	ErrorDialogBgColor       = tcell.ColorRed
	ErrorDialogButtonBgColor = tcell.ColorSpringGreen
	// terminal.
	TerminalBgColor     = tview.Styles.PrimitiveBackgroundColor
	TerminalFgColor     = tview.Styles.PrimaryTextColor
	TerminalBorderColor = tview.Styles.PrimitiveBackgroundColor
	// table header.
	TableHeaderBgColor = tcell.ColorSpringGreen
	TableHeaderFgColor = tview.Styles.PrimaryTextColor
	// progress bar.
	PrgBgColor       = tview.Styles.PrimaryTextColor
	PrgBarColor      = tcell.ColorFuchsia
	PrgBarEmptyColor = tcell.ColorWhite
	PrgBarOKColor    = tcell.ColorLime
	PrgBarWarnColor  = tcell.ColorYellow
	PrgBarCritColor  = tcell.ColorRed
	// dropdown.
	DropDownUnselected = tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)
	DropDownSelected   = tcell.StyleDefault.Background(tcell.ColorPurple).Foreground(tview.Styles.PrimaryTextColor)
	// other primitives.
	InputFieldBgColor = tcell.ColorGray
	ButtonBgColor     = tcell.ColorSpringGreen
)

// GetColorName returns convert tcell color to its name.
func GetColorName(color tcell.Color) string {
	for name, c := range tcell.ColorNames {
		if c == color {
			return name
		}
	}
	return ""
}

// GetColorHex shall returns convert tcell color to its hex useful for textview primitives,
// however, for windows nodes it will return color name.
func GetColorHex(color tcell.Color) string {
	for name, c := range tcell.ColorNames {
		if c == color {
			return name
		}
	}
	return ""
}

package help

import (
	"fmt"

	"github.com/sjzar/chatlog/internal/ui/style"

	"github.com/rivo/tview"
)

const (
	Title     = "help"
	ShowTitle = "帮助"
	Content   = `[yellow]Chatlog 使用指南[white]

[green]基本操作:[white]
• 使用 [yellow]←→[white] 键在主菜单和帮助页面之间切换
• 使用 [yellow]↑↓[white] 键在菜单项之间移动
• 按 [yellow]Enter[white] 选择菜单项
• 按 [yellow]Esc[white] 返回上一级菜单
• 按 [yellow]Ctrl+C[white] 退出程序

[green]使用步骤:[white]

[yellow]1. 下载并安装微信客户端[white]

[yellow]2. 迁移手机微信聊天记录[white]
   手机微信上操作 [yellow]我 - 设置 - 通用 - 聊天记录迁移与备份 - 迁移 - 迁移到电脑[white]。
   这一步的目的是将手机中的聊天记录传输到电脑上。
   可以放心操作，不会影响到手机上的聊天记录。

[yellow]3. 解密数据[white]
   重新打开 chatlog，选择"解密数据"菜单项，程序会使用获取的密钥解密微信数据库文件。
   解密后的文件会保存到工作目录中（可在设置中修改）。

[yellow]4. 启动 HTTP 服务[white]
   选择"启动 HTTP 服务"菜单项，启动 HTTP 和 MCP 服务。
   启动后可以通过浏览器访问 http://localhost:5030 查看聊天记录。

[yellow]5. 设置选项[white]
   选择"设置"菜单项，可以配置:
   • HTTP 服务端口 - 更改 HTTP 服务的监听端口
   • 工作目录 - 更改解密数据的存储位置

[green]HTTP API 使用:[white]
• 聊天记录: [yellow]GET http://localhost:5030/api/v1/chatlog?time=2023-01-01&talker=wxid_xxx[white]
• 联系人列表: [yellow]GET http://localhost:5030/api/v1/contact[white]
• 群聊列表: [yellow]GET http://localhost:5030/api/v1/chatroom[white]
• 会话列表: [yellow]GET http://localhost:5030/api/v1/session[white]

[green]MCP 集成:[white]
Chatlog 支持 Model Context Protocol，可与支持 MCP 的 AI 助手集成。
通过 MCP，AI 助手可以直接查询您的聊天记录、联系人和群聊信息。

[green]常见问题:[white]
• 如果获取密钥失败，请确保微信程序正在运行
• 如果解密失败，请检查密钥是否正确获取
• 如果 HTTP 服务启动失败，请检查端口是否被占用
• 数据目录和工作目录会自动保存，下次启动时自动加载

[green]数据安全:[white]
• 所有数据处理均在本地完成，不会上传到任何外部服务器
• 请妥善保管解密后的数据，避免隐私泄露
`
)

type Help struct {
	*tview.TextView
	title string
}

func New() *Help {
	help := &Help{
		TextView: tview.NewTextView(),
		title:    Title,
	}

	help.SetDynamicColors(true)
	help.SetRegions(true)
	help.SetWrap(true)
	help.SetTextAlign(tview.AlignLeft)
	help.SetBorder(true)
	help.SetBorderColor(style.BorderColor)
	help.SetTitle(ShowTitle)

	fmt.Fprint(help, Content)

	return help
}

package footer

import (
	"fmt"

	"github.com/sjzar/chatlog/internal/ui/style"
	"github.com/sjzar/chatlog/pkg/version"

	"github.com/rivo/tview"
)

const (
	Title = "footer"
)

type Footer struct {
	*tview.Flex
	title     string
	copyRight *tview.TextView
	help      *tview.TextView
}

func New() *Footer {
	footer := &Footer{
		Flex:      tview.NewFlex(),
		title:     Title,
		copyRight: tview.NewTextView(),
		help:      tview.NewTextView(),
	}

	footer.copyRight.
		SetDynamicColors(true).
		SetWrap(true).
		SetTextAlign(tview.AlignLeft)
	footer.copyRight.
		SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
	footer.copyRight.SetText(fmt.Sprintf("[%s::b]%s[-:-:-]", style.GetColorHex(style.PageHeaderFgColor), fmt.Sprintf(" @ Sarv's Chatlog %s", version.Version)))

	footer.help.
		SetDynamicColors(true).
		SetWrap(true).
		SetTextAlign(tview.AlignRight)
	footer.help.
		SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor)

	fmt.Fprintf(footer.help,
		"[%s::b]↑/↓[%s::b]: 导航  [%s::b]←/→[%s::b]: 切换标签  [%s::b]Enter[%s::b]: 选择  [%s::b]ESC[%s::b]: 返回  [%s::b]Ctrl+C[%s::b]: 退出",
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
		style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
	)

	footer.
		AddItem(footer.copyRight, 0, 1, false).
		AddItem(footer.help, 0, 1, false)

	return footer
}

func (f *Footer) SetCopyRight(text string) {
	f.copyRight.SetText(text)
}

func (f *Footer) SetHelp(text string) {
	f.help.SetText(text)
}

package process

import (
	"github.com/sjzar/chatlog/internal/wechat/model"
	"github.com/sjzar/chatlog/internal/wechat/process/darwin"
	"github.com/sjzar/chatlog/internal/wechat/process/windows"
)

type Detector interface {
	FindProcesses() ([]*model.Process, error)
}

// NewDetector 创建适合当前平台的检测器
func NewDetector(platform string) Detector {
	// 根据平台返回对应的实现
	switch platform {
	case "windows":
		return windows.NewDetector()
	case "darwin":
		return darwin.NewDetector()
	default:
		// 默认返回一个空实现
		return &nullDetector{}
	}
}

// nullDetector 空实现
type nullDetector struct{}

func (d *nullDetector) FindProcesses() ([]*model.Process, error) {
	return nil, nil
}

func (d *nullDetector) GetProcessInfo(pid uint32) (*model.Process, error) {
	return nil, nil
}

package model

import "time"

// CREATE TABLE SessionAbstract(
// m_nsUserName TEXT PRIMARY KEY,
// m_uUnReadCount INTEGER,
// m_bShowUnReadAsRedDot INTEGER,
// m_bMarkUnread INTEGER,
// m_uLastTime INTEGER,
// strRes1 TEXT,
// strRes2 TEXT,
// strRes3 TEXT,
// intRes1 INTEGER,
// intRes2 INTEGER,
// intRes3 INTEGER,
// _packed_MMSessionInfo BLOB
// )
type SessionDarwinV3 struct {
	M_nsUserName string `json:"m_nsUserName"`
	M_uLastTime  int    `json:"m_uLastTime"`

	// M_uUnReadCount        int    `json:"m_uUnReadCount"`
	// M_bShowUnReadAsRedDot int    `json:"m_bShowUnReadAsRedDot"`
	// M_bMarkUnread         int    `json:"m_bMarkUnread"`
	// StrRes1               string `json:"strRes1"`
	// StrRes2               string `json:"strRes2"`
	// StrRes3               string `json:"strRes3"`
	// IntRes1               int    `json:"intRes1"`
	// IntRes2               int    `json:"intRes2"`
	// IntRes3               int    `json:"intRes3"`
	// PackedMMSessionInfo   string `json:"_packed_MMSessionInfo"` // TODO: decode
}

func (s *SessionDarwinV3) Wrap() *Session {
	return &Session{
		UserName: s.M_nsUserName,
		NOrder:   s.M_uLastTime,
		NTime:    time.Unix(int64(s.M_uLastTime), 0),
	}
}

package model

import (
	"path/filepath"
)

type Media struct {
	Type       string `json:"type"` // 媒体类型：image, video, voice, file
	Key        string `json:"key"`  // MD5
	Path       string `json:"path"`
	Name       string `json:"name"`
	Size       int64  `json:"size"`
	Data       []byte `json:"data"` // for voice
	ModifyTime int64  `json:"modifyTime"`
}

type MediaV3 struct {
	Type       string `json:"type"`
	Key        string `json:"key"`
	Dir1       string `json:"dir1"`
	Dir2       string `json:"dir2"`
	Name       string `json:"name"`
	ModifyTime int64  `json:"modifyTime"`
}

func (m *MediaV3) Wrap() *Media {

	var path string
	switch m.Type {
	case "image":
		path = filepath.Join("FileStorage", "MsgAttach", m.Dir1, "Image", m.Dir2, m.Name)
	case "video":
		path = filepath.Join("FileStorage", "Video", m.Dir2, m.Name)
	case "file":
		path = filepath.Join("FileStorage", "File", m.Dir2, m.Name)
	}

	return &Media{
		Type:       m.Type,
		Key:        m.Key,
		ModifyTime: m.ModifyTime,
		Path:       path,
		Name:       m.Name,
	}
}

package chatlog

import (
	"io"
	"os"
	"path/filepath"
	"time"

	"github.com/sjzar/chatlog/pkg/util"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

var Debug bool

func initLog(cmd *cobra.Command, args []string) {
	zerolog.SetGlobalLevel(zerolog.InfoLevel)

	if Debug {
		zerolog.SetGlobalLevel(zerolog.DebugLevel)
	}

	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}

func initTuiLog(cmd *cobra.Command, args []string) {
	logOutput := io.Discard

	debug, _ := cmd.Flags().GetBool("debug")
	if debug {
		logpath := util.DefaultWorkDir("")
		util.PrepareDir(logpath)
		logFD, err := os.OpenFile(filepath.Join(logpath, "chatlog.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
		if err != nil {
			panic(err)
		}
		logOutput = logFD
	}

	log.Logger = log.Output(zerolog.ConsoleWriter{Out: logOutput, NoColor: true, TimeFormat: time.RFC3339})
	logrus.SetOutput(logOutput)
}

package conf

import (
	"log"
	"os"
	"sync"

	"github.com/sjzar/chatlog/pkg/config"
)

const (
	ConfigName   = "chatlog"
	ConfigType   = "json"
	EnvConfigDir = "CHATLOG_DIR"
)

// Service 配置服务
type Service struct {
	configPath string
	config     *Config
	mu         sync.RWMutex
}

// NewService 创建配置服务
func NewService(configPath string) (*Service, error) {

	service := &Service{
		configPath: configPath,
	}

	if err := service.Load(); err != nil {
		return nil, err
	}

	return service, nil
}

// Load 加载配置
func (s *Service) Load() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	configPath := s.configPath
	if configPath == "" {
		configPath = os.Getenv(EnvConfigDir)
	}
	if err := config.Init(ConfigName, ConfigType, configPath); err != nil {
		log.Fatal(err)
	}

	conf := &Config{}
	if err := config.Load(conf); err != nil {
		log.Fatal(err)
	}
	conf.ConfigDir = config.ConfigPath
	s.config = conf
	return nil
}

// GetConfig 获取配置副本
func (s *Service) GetConfig() *Config {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// 返回配置副本
	configCopy := *s.config
	return &configCopy
}

package mcp

const (
	JsonRPCVersion = "2.0"
)

// Documents: https://modelcontextprotocol.io/docs/concepts/transports

// Request
//
//	{
//		jsonrpc: "2.0",
//		id: number | string,
//		method: string,
//		params?: object
//	}
type Request struct {
	JsonRPC string      `json:"jsonrpc"`
	ID      interface{} `json:"id"`
	Method  string      `json:"method"`
	Params  interface{} `json:"params,omitempty"`
}

// Response
//
//	{
//		jsonrpc: "2.0",
//		id: number | string,
//		result?: object,
//		error?: {
//			code: number,
//			message: string,
//			data?: unknown
//		}
//	}
type Response struct {
	JsonRPC string      `json:"jsonrpc"`
	ID      interface{} `json:"id"`
	Result  interface{} `json:"result,omitempty"`
	Error   *Error      `json:"error,omitempty"`
}

func NewResponse(id interface{}, result interface{}) *Response {
	return &Response{
		JsonRPC: JsonRPCVersion,
		ID:      id,
		Result:  result,
	}
}

// Notifications
//
//	{
//		jsonrpc: "2.0",
//		method: string,
//		params?: object
//	}
type Notification struct {
	JsonRPC string      `json:"jsonrpc"`
	Method  string      `json:"method"`
	Params  interface{} `json:"params,omitempty"`
}

//go:build !windows

package windows

import (
	"context"

	"github.com/sjzar/chatlog/internal/wechat/model"
)

func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	return "", nil
}

package errors

import (
	"net/http"
	"time"
)

var (
	ErrTalkerEmpty     = New(nil, http.StatusBadRequest, "talker empty").WithStack()
	ErrKeyEmpty        = New(nil, http.StatusBadRequest, "key empty").WithStack()
	ErrMediaNotFound   = New(nil, http.StatusNotFound, "media not found").WithStack()
	ErrKeyLengthMust32 = New(nil, http.StatusBadRequest, "key length must be 32 bytes").WithStack()
)

// 数据库初始化相关错误
func DBFileNotFound(path, pattern string, cause error) *Error {
	return Newf(cause, http.StatusNotFound, "db file not found %s: %s", path, pattern).WithStack()
}

func DBConnectFailed(path string, cause error) *Error {
	return Newf(cause, http.StatusInternalServerError, "db connect failed: %s", path).WithStack()
}

func DBInitFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "db init failed").WithStack()
}

func TalkerNotFound(talker string) *Error {
	return Newf(nil, http.StatusNotFound, "talker not found: %s", talker).WithStack()
}

func DBCloseFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "db close failed").WithStack()
}

func QueryFailed(query string, cause error) *Error {
	return Newf(cause, http.StatusInternalServerError, "query failed: %s", query).WithStack()
}

func ScanRowFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "scan row failed").WithStack()
}

func TimeRangeNotFound(start, end time.Time) *Error {
	return Newf(nil, http.StatusNotFound, "time range not found: %s - %s", start, end).WithStack()
}

func MediaTypeUnsupported(_type string) *Error {
	return Newf(nil, http.StatusBadRequest, "unsupported media type: %s", _type).WithStack()
}

func ChatRoomNotFound(key string) *Error {
	return Newf(nil, http.StatusNotFound, "chat room not found: %s", key).WithStack()
}

func ContactNotFound(key string) *Error {
	return Newf(nil, http.StatusNotFound, "contact not found: %s", key).WithStack()
}

func InitCacheFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "init cache failed").WithStack()
}

func FileGroupNotFound(name string) *Error {
	return Newf(nil, http.StatusNotFound, "file group not found: %s", name).WithStack()
}

package model

import "path/filepath"

// CREATE TABLE HlinkMediaRecord(
// mediaMd5 TEXT,
// mediaSize INTEGER,
// inodeNumber INTEGER,
// modifyTime INTEGER ,
// CONSTRAINT _Md5_Size UNIQUE (mediaMd5,mediaSize)
// )
// CREATE TABLE HlinkMediaDetail(
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
// inodeNumber INTEGER,
// relativePath TEXT,
// fileName TEXT
// )
type MediaDarwinV3 struct {
	MediaMd5     string `json:"mediaMd5"`
	MediaSize    int64  `json:"mediaSize"`
	InodeNumber  int64  `json:"inodeNumber"`
	ModifyTime   int64  `json:"modifyTime"`
	RelativePath string `json:"relativePath"`
	FileName     string `json:"fileName"`
}

func (m *MediaDarwinV3) Wrap() *Media {

	path := filepath.Join("Message/MessageTemp", m.RelativePath, m.FileName)
	name := filepath.Base(path)

	return &Media{
		Type:       "",
		Key:        m.MediaMd5,
		Size:       m.MediaSize,
		ModifyTime: m.ModifyTime,
		Path:       path,
		Name:       name,
	}
}

//go:build !windows

package windows

import (
	"context"

	"github.com/sjzar/chatlog/internal/wechat/model"
)

func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
	return "", nil
}

package glance

import (
	"os/exec"
	"strings"
)

// IsSIPDisabled checks if System Integrity Protection (SIP) is disabled on macOS.
// Returns true if SIP is disabled, false if it's enabled or if the status cannot be determined.
func IsSIPDisabled() bool {
	// Run the csrutil status command to check SIP status
	cmd := exec.Command("csrutil", "status")
	output, err := cmd.CombinedOutput()
	if err != nil {
		// If there's an error running the command, assume SIP is enabled
		return false
	}

	// Convert output to string and check if SIP is disabled
	outputStr := strings.ToLower(string(output))

	// $ csrutil status
	// System Integrity Protection status: disabled.

	// If the output contains "disabled", SIP is disabled
	if strings.Contains(outputStr, "system integrity protection status: disabled") {
		return true
	}

	// Check for partial SIP disabling - some configurations might have specific protections disabled
	if strings.Contains(outputStr, "disabled") && strings.Contains(outputStr, "debugging") {
		return true
	}

	// By default, assume SIP is enabled
	return false
}

package model

import "time"

// 注意，v4 session 是独立数据库文件
// CREATE TABLE SessionTable(
// username TEXT PRIMARY KEY,
// type INTEGER,
// unread_count INTEGER,
// unread_first_msg_srv_id INTEGER,
// is_hidden INTEGER,
// summary TEXT,
// draft TEXT,
// status INTEGER,
// last_timestamp INTEGER,
// sort_timestamp INTEGER,
// last_clear_unread_timestamp INTEGER,
// last_msg_locald_id INTEGER,
// last_msg_type INTEGER,
// last_msg_sub_type INTEGER,
// last_msg_sender TEXT,
// last_sender_display_name TEXT,
// last_msg_ext_type INTEGER
// )
type SessionV4 struct {
	Username              string `json:"username"`
	Summary               string `json:"summary"`
	LastTimestamp         int    `json:"last_timestamp"`
	LastMsgSender         string `json:"last_msg_sender"`
	LastSenderDisplayName string `json:"last_sender_display_name"`

	// Type                     int    `json:"type"`
	// UnreadCount              int    `json:"unread_count"`
	// UnreadFirstMsgSrvID      int    `json:"unread_first_msg_srv_id"`
	// IsHidden                 int    `json:"is_hidden"`
	// Draft                    string `json:"draft"`
	// Status                   int    `json:"status"`
	// SortTimestamp            int    `json:"sort_timestamp"`
	// LastClearUnreadTimestamp int    `json:"last_clear_unread_timestamp"`
	// LastMsgLocaldID          int    `json:"last_msg_locald_id"`
	// LastMsgType              int    `json:"last_msg_type"`
	// LastMsgSubType           int    `json:"last_msg_sub_type"`
	// LastMsgExtType           int    `json:"last_msg_ext_type"`
}

func (s *SessionV4) Wrap() *Session {
	return &Session{
		UserName: s.Username,
		NOrder:   s.LastTimestamp,
		NickName: s.LastSenderDisplayName,
		Content:  s.Summary,
		NTime:    time.Unix(int64(s.LastTimestamp), 0),
	}
}

package model

import "path/filepath"

type MediaV4 struct {
	Type       string `json:"type"`
	Key        string `json:"key"`
	Dir1       string `json:"dir1"`
	Dir2       string `json:"dir2"`
	Name       string `json:"name"`
	Size       int64  `json:"size"`
	ModifyTime int64  `json:"modifyTime"`
}

func (m *MediaV4) Wrap() *Media {

	var path string
	switch m.Type {
	case "image":
		path = filepath.Join("msg", "attach", m.Dir1, m.Dir2, "Img", m.Name)
	case "video":
		path = filepath.Join("msg", "video", m.Dir1, m.Name)
	case "file":
		path = filepath.Join("msg", "file", m.Dir1, m.Name)
	}

	return &Media{
		Type:       m.Type,
		Key:        m.Key,
		Path:       path,
		Name:       m.Name,
		Size:       m.Size,
		ModifyTime: m.ModifyTime,
	}
}

package chatlog

import (
	"fmt"

	"github.com/sjzar/chatlog/pkg/version"

	"github.com/spf13/cobra"
)

func init() {
	rootCmd.AddCommand(versionCmd)
	versionCmd.Flags().BoolVarP(&versionM, "module", "m", false, "module version information")
}

var versionM bool
var versionCmd = &cobra.Command{
	Use:   "version [-m]",
	Short: "Show the version of chatlog",
	Run: func(cmd *cobra.Command, args []string) {
		if versionM {
			fmt.Println(version.GetMore(true))
		} else {
			fmt.Printf("chatlog %s\n", version.GetMore(false))
		}
	},
}

//go:build !windows

package windows

import (
	"github.com/shirou/gopsutil/v4/process"
	"github.com/sjzar/chatlog/internal/wechat/model"
)

func initializeProcessInfo(p *process.Process, info *model.Process) error {
	return nil
}

package model

// CREATE TABLE contact(
// id INTEGER PRIMARY KEY,
// username TEXT,
// local_type INTEGER,
// alias TEXT,
// encrypt_username TEXT,
// flag INTEGER,
// delete_flag INTEGER,
// verify_flag INTEGER,
// remark TEXT,
// remark_quan_pin TEXT,
// remark_pin_yin_initial TEXT,
// nick_name TEXT,
// pin_yin_initial TEXT,
// quan_pin TEXT,
// big_head_url TEXT,
// small_head_url TEXT,
// head_img_md5 TEXT,
// chat_room_notify INTEGER,
// is_in_chat_room INTEGER,
// description TEXT,
// extra_buffer BLOB,
// chat_room_type INTEGER
// )
type ContactV4 struct {
	UserName  string `json:"username"`
	Alias     string `json:"alias"`
	Remark    string `json:"remark"`
	NickName  string `json:"nick_name"`
	LocalType int    `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
}

func (c *ContactV4) Wrap() *Contact {
	return &Contact{
		UserName: c.UserName,
		Alias:    c.Alias,
		Remark:   c.Remark,
		NickName: c.NickName,
		IsFriend: c.LocalType != 3,
	}
}

package repository

import (
	"context"

	"github.com/sjzar/chatlog/internal/model"
)

func (r *Repository) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
	return r.ds.GetSessions(ctx, key, limit, offset)
}

package model

// CREATE TABLE WCContact(
// m_nsUsrName TEXT PRIMARY KEY ASC,
// m_uiConType INTEGER,
// nickname TEXT,
// m_nsFullPY TEXT,
// m_nsShortPY TEXT,
// m_nsRemark TEXT,
// m_nsRemarkPYFull TEXT,
// m_nsRemarkPYShort TEXT,
// m_uiCertificationFlag INTEGER,
// m_uiSex INTEGER,
// m_uiType INTEGER,
// m_nsImgStatus TEXT,
// m_uiImgKey INTEGER,
// m_nsHeadImgUrl TEXT,
// m_nsHeadHDImgUrl TEXT,
// m_nsHeadHDMd5 TEXT,
// m_nsChatRoomMemList TEXT,
// m_nsChatRoomAdminList TEXT,
// m_uiChatRoomStatus INTEGER,
// m_nsChatRoomDesc TEXT,
// m_nsDraft TEXT,
// m_nsBrandIconUrl TEXT,
// m_nsGoogleContactName TEXT,
// m_nsAliasName TEXT,
// m_nsEncodeUserName TEXT,
// m_uiChatRoomVersion INTEGER,
// m_uiChatRoomMaxCount INTEGER,
// m_uiChatRoomType INTEGER,
// m_patSuffix TEXT,
// richChatRoomDesc TEXT,
// _packed_WCContactData BLOB,
// openIMInfo BLOB
// )
type ContactDarwinV3 struct {
	M_nsUsrName   string `json:"m_nsUsrName"`
	Nickname      string `json:"nickname"`
	M_nsRemark    string `json:"m_nsRemark"`
	M_uiSex       int    `json:"m_uiSex"`
	M_nsAliasName string `json:"m_nsAliasName"`
}

func (c *ContactDarwinV3) Wrap() *Contact {
	return &Contact{
		UserName: c.M_nsUsrName,
		Alias:    c.M_nsAliasName,
		Remark:   c.M_nsRemark,
		NickName: c.Nickname,
		IsFriend: true,
	}
}

package repository

import (
	"context"

	"github.com/sjzar/chatlog/internal/model"
)

func (r *Repository) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
	return r.ds.GetMedia(ctx, _type, key)
}

package model

type Process struct {
	PID         uint32
	ExePath     string
	Platform    string
	Version     int
	FullVersion string
	Status      string
	DataDir     string
	AccountName string
}

// 平台常量定义
const (
	PlatformWindows = "windows"
	PlatformMacOS   = "darwin"
)

const (
	StatusInit    = ""
	StatusOffline = "offline"
	StatusOnline  = "online"
)

package errors

import "net/http"

func OpenFileFailed(path string, cause error) *Error {
	return Newf(cause, http.StatusInternalServerError, "failed to open file: %s", path).WithStack()
}

func StatFileFailed(path string, cause error) *Error {
	return Newf(cause, http.StatusInternalServerError, "failed to stat file: %s", path).WithStack()
}

func ReadFileFailed(path string, cause error) *Error {
	return Newf(cause, http.StatusInternalServerError, "failed to read file: %s", path).WithStack()
}

func IncompleteRead(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "incomplete header read during decryption").WithStack()
}

func WriteOutputFailed(cause error) *Error {
	return New(cause, http.StatusInternalServerError, "failed to write output").WithStack()
}

package errors

import "net/http"

func InvalidArg(arg string) error {
	return Newf(nil, http.StatusBadRequest, "invalid argument: %s", arg)
}

func HTTPShutDown(cause error) error {
	return Newf(cause, http.StatusInternalServerError, "http server shut down")
}

package main

import (
	"log"

	"github.com/sjzar/chatlog/cmd/chatlog"
)

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	chatlog.Execute()
}

package dbm

type Group struct {
	Name      string
	Pattern   string
	BlackList []string
}

func (e *V4Extractor) worker (ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {}

This code segment functions to: Scan memory regions for a specific byte pattern, extract and validate adjacent pointer values, and communicate discovered cryptographic material through concurrent channels while monitoring for termination signals.

```go
[TASK_BEGIN]
func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {
	for {
		select {
		case <-ctx.Done():
			return
		case data, ok := <-memoryChannel:
			if !ok {
				return
			}

			// Search for key pattern 0x68 followed by address bytes
			for i := 0; i < len(data)-5; i++ {
				if data[i] == 0x68 {
					addrBytes := data[i+1 : i+5]
					address := binary.LittleEndian.Uint32(addrBytes)

					// Validate found address
					if key := e.validateKey(handle, uint64(address)); key != "" {
						select {
						case resultChannel <- key:
							return // Success exit
						case <-ctx.Done():
							return // Context canceled
						}
					}
				}
			}
		}
	}
}
[TASK_END]
```

func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {
	keyPattern := []byte{
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	}
	ptrSize := 8
	littleEndianFunc := binary.LittleEndian.Uint64
	for {
		select {
		case <-ctx.Done():
			return
		case memory, ok := <-memoryChannel:
			if !ok {
				return
			}
			index := len(memory)
			for {
				select {
				case <-ctx.Done():
					return
				default:
				}
				index = bytes.LastIndex(memory[:index], keyPattern)
				if index == -1 || index-ptrSize < 0 {
					break
				}
				ptrValue := littleEndianFunc(memory[index-ptrSize : index])
				if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
					if key := e.validateKey(handle, ptrValue); key != "" {
						select {
						case resultChannel <- key:
							log.Debug().Msg("Valid key found: " + key)
							return
						default:
						}
					}
				}
				index -= 1
			}
		}
	}
}

func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {
	for {
		select {
		case <-ctx.Done():
			return
		case data, ok := <-memoryChannel:
			if !ok {
				return
			}
			for i := 0; i < len(data)-5; i++ {
				if data[i] == 0x68 {
					addrBytes := data[i+1 : i+5]
					address := binary.LittleEndian.Uint32(addrBytes)
					if key := e.validateKey(handle, uint64(address)); key != "" {
						select {
						case resultChannel <- key:
							return
						case <-ctx.Done():
							return
						}
					}
				}
			}
		}
	}
}