package mr

import (
	"bytes"
	"encoding/json"
	"fmt"
	"hash/fnv"
	"io"
	"log"
	"net/rpc"
	"os"
	"path/filepath"
	"strings"
)

// Map functions return a slice of KeyValue.
type KeyValue struct {
	Key   string
	Value string
}

// use ihash(key) % NReduce to choose the reduce
// task number for each KeyValue emitted by Map.
func ihash(key string) int {
	h := fnv.New32a()
	h.Write([]byte(key))
	return int(h.Sum32() & 0x7fffffff)
}

// main/mrworker.go calls this function.
func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {
	// Your worker implementation here.
	taskType, inFilename, nReduce := RequestTask()
	fmt.Printf("Perform %s task on: %s", taskType, inFilename)
	file, err := os.Open(inFilename)
	if err != nil {
		log.Fatalf("cannot open %v", inFilename)
	}
	content, err := io.ReadAll(file)
	if err != nil {
		log.Fatalf("cannot read %v", inFilename)
	}
	file.Close()
	if taskType == "map" {
		intermediateFiles := []string{}
		kva := mapf(inFilename, string(content))
		//hash the map job id
		mapTaskIdx := ihash(inFilename)
		for i := range kva {
			log.Printf("%s: %s", kva[i].Key, kva[i].Value)
			idx := ihash(kva[i].Key) % nReduce
			mapOutFn := fmt.Sprintf("mr-%d-%d", mapTaskIdx, idx)
			// add to intermediate file list
			intermediateFiles = append(intermediateFiles, mapOutFn)
			// write intermidiate files
			data, err := json.MarshalIndent(kva[i], "", " ")
			if err != nil {
				log.Fatalf("failed to serialize map to JSON: %w", err)
			}
			if err = WriteTempAtomic(mapOutFn, data); err != nil {
				log.Fatalf("failed to write KV map to file: %w", err)
			}
		}
		SetTaskDone("map", inFilename, intermediateFiles)
		return

	} else if taskType == "reduce" {
		intermediate := []KeyValue{}

		// Prepare output file
		oFilename := strings.Split(inFilename, ",")[1]
		var buffer bytes.Buffer
		i := 0
		for i < len(intermediate) {
			j := i + 1
			for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
				j++
			}
			values := []string{}
			for k := i; k < j; k++ {
				values = append(values, intermediate[k].Value)
			}
			output := reducef(intermediate[i].Key, values)

			// this is the correct format for each line of Reduce output.
			fmt.Fprintf(&buffer, "%v %v\n", intermediate[i].Key, output)

			i = j
		}
		if err = WriteTempAtomic(oFilename, buffer.Bytes()); err != nil {
			log.Fatalf("failed to write KV map to file: %w", err)
		}
		SetTaskDone("reduce", inFilename, []string{})
		return
	}
}

func WriteTempAtomic(fn string, data []byte) error {
	// Create a temporary file in the target directory
	currentDir, err := os.Getwd()
	tempFile, err := os.CreateTemp(currentDir, fn)
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	tempPath := tempFile.Name()

	if _, err := tempFile.Write(data); err != nil {
		tempFile.Close()
		os.Remove(tempPath)
		return fmt.Errorf("failed to write to temp file: %w", err)
	}

	// Ensure the file is fully written to disk
	if err := tempFile.Sync(); err != nil {
		tempFile.Close()
		os.Remove(tempPath)
		return fmt.Errorf("failed to sync temp file: %w", err)
	}

	if err := tempFile.Close(); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to close temp file: %w", err)
	}

	// Compute the target file path
	targetPath := filepath.Join(currentDir, fn)

	// Atomically replace the target file with the temporary file
	if err := os.Rename(tempPath, targetPath); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to rename temp file to target: %w", err)
	}
	return nil
}

// RPC call to set the task status to done
func SetTaskDone(taskType string, filename string, intermediateFiles []string) {
	args := RpcArgument{}
	args.Method = "set_task_done"
	args.TaskType = taskType
	args.Filename = filename
	args.IntermediateFiles = intermediateFiles
	reply := RpcReply{}
	ok := call("Coordinator.SetTaskDone", &args, &reply)
	if ok {
		fmt.Printf("ok")
	} else {
		fmt.Printf("call failed!\n")
	}
}

// RPC call to request task from coordinator
func RequestTask() (string, string, int) {
	// declare an arg and reply
	args := RpcArgument{}
	args.Method = "request_task"
	reply := RpcReply{}

	ok := call("Coordinator.GetTask", &args, &reply)
	if ok {
		fmt.Printf("reply.TaskType %v\n", reply.TaskType)
		fmt.Printf("reply.InFilename %v\n", reply.Filename)
	} else {
		fmt.Printf("call failed!\n")
	}
	return reply.TaskType, reply.Filename, reply.NReduce
}

// send an RPC request to the coordinator, wait for the response.
// usually returns true.
// returns false if something goes wrong.
func call(rpcname string, args interface{}, reply interface{}) bool {
	// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
	sockname := coordinatorSock()
	c, err := rpc.DialHTTP("unix", sockname)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	defer c.Close()

	err = c.Call(rpcname, args, reply)
	if err == nil {
		return true
	}

	fmt.Println(err)
	return false
}
