package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/logger/logger"
)

var (
	schemeFlag     = flag.Bool("S", false, "URL scheme (http or https)")
	apiURL         = flag.String("z", "", "Job Arranger Manager API URL")
	port           = flag.String("p", "", "Specify port number of Job Arranger Manager")
	userFlag       = flag.String("U", "", "User name")
	passFlag       = flag.String("P", "", "Password")
	registryNumber = flag.String("r", "", "registry-number")
	variableFormat = flag.Bool("e", false, "enable variable-format")
	helpFlag       = flag.Bool("h", false, "Show help")
	versionFlag    = flag.Bool("V", false, "Show version")
)

func main() {
	flag.Parse()

	if *helpFlag {
		jobargHelp()
		return
	}
	if *versionFlag {
		jobargVersion()
		return
	}

	if *apiURL == "" || *userFlag == "" || *passFlag == "" || *registryNumber == "" {
		fmt.Fprintln(os.Stderr, "Error: Missing required parameters")
		jobargHelp()
		os.Exit(1)
	}

	if !strings.HasPrefix(*apiURL, "http://") && !strings.HasPrefix(*apiURL, "https://") {
		host := *apiURL
		port := *port

		if port == "" {
			// set default port based on scheme
			switch *schemeFlag {
			case false:
				port = "80"
			case true:
				port = "443"
			default:
				port = "80" // fallback default
			}
		}

		// append port if not empty
		if port != "" {
			host = fmt.Sprintf("%s:%s", host, port)
		}

		scheme := "http"
		if *schemeFlag {
			scheme = "https"
		}

		*apiURL = fmt.Sprintf("%s://%s/jobarranger/api/JobargModule/handleRequest", scheme, host)

	}

	// Init logger
	if err := logger.InitLogger(
		"",
		"",
		"console",
		10*1024*1024,
		int(logger.LogLevelInfo),
		logger.TargetTypeModule,
	); err != nil {
		fmt.Fprintf(os.Stderr, "failed to initialize logger: %v", err)
		os.Exit(1)
	}

	logger.WriteLogHelper(logger.LogLevelDebug, "DEBUG LOG TESTING")

	// Build json payload for zabbix login
	payload, err := buildJSONPayloadForZabbixLogin(*userFlag, *passFlag)
	if err != nil {
		log.Fatalf("Error building payload: %v", err)
	}

	setupSignalHandler()

	respBody, err := sendToPHPAPI(*apiURL, payload)
	if err != nil {
		log.Fatalf("Error sending request: %v", err)
	}

	logger.WriteLogHelper(logger.LogLevelDebug, "Response from PHP API: %s", respBody)

	resRaw, err := checkResponseFromJazWeb(respBody, "zabbix")
	if err != nil {
		if errResp, ok := resRaw.(common.ErrorResponseWithMsg); ok {
			fmt.Printf("Error type: %s, message: %s\n", errResp.Error.Type, errResp.Error.Message)
		} else {
			fmt.Fprintf(os.Stderr, "Unexpected error: %v\n", err)
		}
		os.Exit(1)
	}

	resZabbix, ok := resRaw.(common.ZabbixOrJobResult)
	if !ok {
		fmt.Fprintf(os.Stderr, "Response is not of type ZabbixOrJobResult\n")
		os.Exit(1)
	}

	payload, err = buildJSONPayloadForJobargGet(*userFlag, *passFlag, *registryNumber, resZabbix.ZabbixAuth)
	if err != nil {
		log.Fatalf("Error sending request: %v", err)
	}

	respBody, err = sendToPHPAPI(*apiURL, payload)
	if err != nil {
		log.Fatalf("Error sending request: %v", err)
	}

	resRaw, err = checkResponseFromJazWeb(respBody, "jaz")
	if err != nil {
		if errResp, ok := resRaw.(common.ErrorResponseWithMsg); ok {
			fmt.Printf("Error type: %s, message: %s\n", errResp.Error.Type, errResp.Error.Message)
		} else {
			fmt.Fprintf(os.Stderr, "Unexpected error: %v\n", err)
		}
		os.Exit(1)
	}

	resJaz, ok := resRaw.(common.ZabbixOrJobResult)
	if !ok {
		fmt.Fprintf(os.Stderr, "Response is not of type ZabbixOrJobResult\n")
		os.Exit(1)
	} else {
		if *resJaz.JobResult == false {
			log.Fatalf("registry number not found")
		}
	}

	// check env variable
	if *variableFormat {
		printExportVariables(resJaz)
	} else {
		logger.WriteLogHelper(logger.LogLevelDebug, "Job Get Result: %s", jobResultToString(resJaz))
		printJobResultFormatted(resJaz)
	}
	// prepare logout payload
	logoutPayload, err := buildJSONPayloadForZabbixLogout(resZabbix.ZabbixAuth)
	if err != nil {
		log.Fatalf("Error building payload: %v", err)
	}

	setupSignalHandler()
	// send logout request
	respLogout, err := sendToPHPAPI(*apiURL, logoutPayload)
	if err != nil {
		log.Fatalf("Error sending request: %v", err)
	}

	logger.WriteLogHelper(logger.LogLevelDebug, "Response from PHP API: %s", respLogout)

	resRaw, err = checkResponseFromJazWeb(respBody, "zabbix")
	if err != nil {
		if errResp, ok := resRaw.(common.ErrorResponseWithMsg); ok {
			fmt.Printf("Error type: %s, message: %s\n", errResp.Error.Type, errResp.Error.Message)
		} else {
			fmt.Fprintf(os.Stderr, "Unexpected error: %v\n", err)
		}
		os.Exit(1)
	}

	resZabbix, ok = resRaw.(common.ZabbixOrJobResult)
	if !ok {
		fmt.Fprintf(os.Stderr, "Response is not of type ZabbixOrJobResult\n")
		os.Exit(1)
	}

}

func printJobResultFormatted(r common.ZabbixOrJobResult) {
	fmt.Printf("jobnetid                 : %s\n", r.JobnetId)
	fmt.Printf("jobnetname               : %s\n", r.JobnetName)
	fmt.Printf("Time of a schedule       : %s\n", formatUnixTimeInt64Ptr(r.ScheduledTime))
	fmt.Printf("Time of a start          : %s\n", formatUnixTimeInt64Ptr(r.StartTime))
	fmt.Printf("Time of a end            : %s\n", formatUnixTimeInt64Ptr(r.EndTime))
	fmt.Printf("The run type of a jobnet : %s\n", mapRunType(r.RunType))
	fmt.Printf("Status of a jobnet       : %s\n", mapStatus(r.Status))
	fmt.Printf("Status of a job          : %s\n", mapJobStatus(r.JobStatus))
	fmt.Printf("Last job return value    : %s\n", r.JobExitCd)
	fmt.Printf("Last job standard output : %s\n", r.StdOut)
	fmt.Printf("Last job standard error  : %s\n", r.StdErr)
}

func formatUnixTimeInt64Ptr(t *int64) string {
	if t == nil || *t == 0 {
		return ""
	}

	return time.Unix(*t, 0).Format("2006-01-02 15:04:05")
}

func jobResultToString(r common.ZabbixOrJobResult) string {
	jobResult := "nil"
	if r.JobResult != nil {
		jobResult = fmt.Sprintf("%v", *r.JobResult)
	}
	return fmt.Sprintf(
		"ZabbixAuth=%s, JobResult=%s, RegistryNumber=%s, RunType=%v, Status=%s, JobStatus=%v, ScheduledTime=%v, StartTime=%v, EndTime=%v, JobnetName=%s, JobnetId=%s, InnerJobId=%s, StdOut=%s, StdErr=%s, JobExitCd=%s",
		r.ZabbixAuth,
		jobResult,
		r.RegistryNumber,
		r.RunType,
		r.Status,
		r.JobStatus,
		r.ScheduledTime,
		r.StartTime,
		r.EndTime,
		r.JobnetName,
		r.JobnetId,
		r.InnerJobId,
		r.StdOut,
		r.StdErr,
		r.JobExitCd,
	)
}

func jobargHelp() {
	fmt.Println("Job Arranger Get Jobnet Info")
	fmt.Println("Usage: jobarg_get -z <server> -U <user> -P <pass> -r <registry-number> [-e]")
}

func jobargVersion() {
	fmt.Printf("Job Arranger Jobnet Get v%s (rev %s) (%s)\n", common.JobargVersion, common.JobargRevision, common.JobargRevDate)
	fmt.Printf("Compiled at %s\n", time.Now().Format("2006-01-02 15:04:05"))
}

func buildJSONPayloadForZabbixLogin(user, pass string) (string, error) {
	payload := common.RequestPayload{
		Zabbix: common.ZabbixRequest{
			JSONRPC: "2.0",
			Method:  "user.login",
			Params: &common.ZabbixParams{ // <-- note the &
				Username: user,
				Password: pass,
			},
			ID: 1,
		},
		JobArranger: map[string]any{},
	}

	bytes, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

func buildJSONPayloadForZabbixLogout(auth string) (string, error) {
	payload := common.RequestPayload{
		Zabbix: common.ZabbixRequest{
			JSONRPC: "2.0",
			Method:  "user.logout",
			Params:  []any{},
			Auth:    auth,
			ID:      1,
		},
		JobArranger: map[string]any{},
	}

	bytes, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

func buildJSONPayloadForJobargGet(user, pass, jobnetid, auth string) (string, error) {
	payload := common.RequestPayload{
		Zabbix: common.ZabbixRequest{
			JSONRPC: "2.0",
			Method:  "execution", // or omit, but empty string is fine
			Params:  nil,         // omit params so PHP won't try to re-login
			Auth:    auth,
			ID:      1,
		},
		JobArranger: map[string]any{
			"method": "jobnetstatusrq",
			"data": map[string]any{
				"username": user,
				"password": pass,
				"jobnetid": jobnetid,
			},
		},
	}

	bytes, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

func extractPort(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return "unknown"
	}
	_, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		return "unknown"
	}
	return port
}

func sendToPHPAPI(apiURL, payload string) (string, error) {
	resp, err := http.Post(apiURL, "application/json", bytes.NewBufferString(payload))
	if err != nil {
		// Detect TLS certificate verification errors
		if strings.Contains(err.Error(), "certificate has expired") ||
			strings.Contains(err.Error(), "x509") {
			return "", fmt.Errorf("TLS certificate verification failed: your server's HTTPS certificate is expired or invalid. ")
		}

		// Connection refused / bad port
		var netErr *net.OpError
		if errors.As(err, &netErr) {
			if netErr.Err.Error() == "connect: connection refused" {
				return "", fmt.Errorf("Connection refused when trying to reach %s. Is the port (%s) correct and the server running?", apiURL, extractPort(apiURL))
			}
		}

		// Default: return the original error
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func checkResponseFromJazWeb(response, use string) (interface{}, error) {

	var errMsg common.ErrorResponseWithMsg
	var resp common.FullResponse
	if err := json.Unmarshal([]byte(response), &errMsg); err == nil && errMsg.Error.Message != "" {
		result := common.ErrorResponseWithMsg{}
		result.Error.Type = errMsg.Error.Type
		result.Error.Message = errMsg.Error.Message
		return result, fmt.Errorf("Error message: %s and type: %s", errMsg.Error.Message, errMsg.Error.Type)
	} else if err := json.Unmarshal([]byte(response), &resp); err != nil {
		return common.ZabbixOrJobResult{}, fmt.Errorf("cannot parse JSON: %w", err)
	}

	result := common.ZabbixOrJobResult{}

	if use == "zabbix" {
		result.ZabbixAuth = resp.Zabbix.Result
	} else {
		result.JobResult = &resp.JobArranger.Result
		result.RunType = resp.JobArranger.RunType
		result.Status = resp.JobArranger.Status
		result.JobStatus = resp.JobArranger.JobStatus
		result.ScheduledTime = resp.JobArranger.ScheduledTime
		result.StartTime = resp.JobArranger.StartTime
		result.EndTime = resp.JobArranger.EndTime
		result.JobnetName = resp.JobArranger.JobnetName
		result.JobnetId = resp.JobArranger.JobnetId
		result.InnerJobId = resp.JobArranger.InnerJobId
		result.StdOut = resp.JobArranger.StdOut
		result.StdErr = resp.JobArranger.StdErr
		result.JobExitCd = resp.JobArranger.JobExitCd
	}

	return result, nil
}

func printExportVariables(res common.ZabbixOrJobResult) {

	printIfNotEmpty("JA_JOBNETID", escapeForShell(res.JobnetId))
	printIfNotEmpty("JA_JOBNETNAME", escapeForShell(res.JobnetName))
	printIfNotEmpty("JA_SCHEDULEDTIME", safeInt64PtrToFormattedStr(res.ScheduledTime))
	printIfNotEmpty("JA_STARTTIME", safeInt64PtrToFormattedStr(res.StartTime))
	printIfNotEmpty("JA_ENDTIME", safeInt64PtrToFormattedStr(res.EndTime))
	printIfNotEmpty("JA_JOBNETRUNTYPE", mapRunType(res.RunType))
	printIfNotEmpty("JA_JOBNETSTATUS", mapStatus(res.Status))
	printIfNotEmpty("JA_JOBSTATUS", mapJobStatus(res.JobStatus))
	printIfNotEmpty("JA_LASTEXITCD", escapeForShell(res.JobExitCd))
	printIfNotEmpty("JA_LASTSTDOUT", escapeForShell(res.StdOut))
	printIfNotEmpty("JA_LASTSTDERR", escapeForShell(res.StdErr))
}

func printIfNotEmpty(key, val string) {
	if val != "" && val != "N/A" {
		fmt.Printf("export %s=\"%s\"\n", key, val)
	}
}

// safeInt64PtrToFormattedStr unchanged
func safeInt64PtrToFormattedStr(i *int64) string {
	if i == nil {
		return ""
	}
	return time.Unix(*i, 0).Format("20060102150405")
}

// mapRunType unchanged
func mapRunType(i *int) string {
	if i == nil {
		return ""
	}
	switch *i {
	case 0:
		return "NORMAL"
	case 1:
		return "IMMEDIATE"
	case 2:
		return "WAIT"
	case 3:
		return "TEST"
	case 4:
		return "SCHEDULED"
	default:
		return strconv.Itoa(*i)
	}
}

// mapJobStatus unchanged
func mapJobStatus(i *int) string {
	if i == nil {
		return ""
	}
	switch *i {
	case 0:
		return "NORMAL"
	case 1:
		return "TIMEOUT"
	case 2:
		return "RUN"
	case 3:
		return "END"
	case 4:
		return "RUNERR"
	case 5:
		return "ENDERR"
	case 6:
		return "ABORT"
	default:
		return strconv.Itoa(*i)
	}
}

// mapStatus unchanged
func mapStatus(s string) string {
	switch s {
	case "0":
		return "BEGIN"
	case "1":
		return "READY"
	case "2":
		return "RUN"
	case "3":
		return "END"
	case "4":
		return "RUNERR"
	case "5":
		return "ENDERR"
	case "6":
		return "ABORT"
	default:
		return s
	}
}

// escapeForShell unchanged
func escapeForShell(s string) string {
	s = strings.ReplaceAll(s, `"`, `\"`)
	s = strings.ReplaceAll(s, "`", "\\`")
	s = strings.ReplaceAll(s, "$", "\\$")
	return s
}

func setupSignalHandler() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	go func() {
		<-c
		log.Println("Received signal, exiting...")
		os.Exit(1)
	}()
}
