// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows

package mi

import (
	"errors"
	"fmt"
	"syscall"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
)

const (
	LocaleEnglish = "en-us"
)

//nolint:gochecknoglobals
var (
	applicationID = UTF16PtrFromString[*uint16]("windows_exporter")
	// destinationOptionsTimeout is the key for the timeout option.
	//
	// https://github.com/microsoft/win32metadata/blob/527806d20d83d3abd43d16cd3fa8795d8deba343/generation/WinSDK/RecompiledIdlHeaders/um/mi.h#L7830
	destinationOptionsTimeout = UTF16PtrFromString[*uint16]("__MI_DESTINATIONOPTIONS_TIMEOUT")

	// destinationOptionsUILocale is the key for the UI locale option.
	//
	// https://github.com/microsoft/win32metadata/blob/527806d20d83d3abd43d16cd3fa8795d8deba343/generation/WinSDK/RecompiledIdlHeaders/um/mi.h#L8248
	destinationOptionsUILocale = UTF16PtrFromString[*uint16]("__MI_DESTINATIONOPTIONS_UI_LOCALE")
)

//nolint:gochecknoglobals
var (
	modMi = windows.NewLazySystemDLL("mi.dll")

	procMIApplicationInitialize = modMi.NewProc("MI_Application_InitializeV1")
)

// Application represents the MI application.
// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_application
type Application struct {
	reserved1 uint64
	reserved2 uintptr
	ft        *ApplicationFT
}

// ApplicationFT represents the function table of the MI application.
// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_applicationft
type ApplicationFT struct {
	Close                          uintptr
	NewSession                     uintptr
	NewHostedProvider              uintptr
	NewInstance                    uintptr
	NewDestinationOptions          uintptr
	NewOperationOptions            uintptr
	NewSubscriptionDeliveryOptions uintptr
	NewSerializer                  uintptr
	NewDeserializer                uintptr
	NewInstanceFromClass           uintptr
	NewClass                       uintptr
}

type DestinationOptions struct {
	reserved1 uint64
	reserved2 uintptr
	ft        *DestinationOptionsFT
}

type DestinationOptionsFT struct {
	Delete                   uintptr
	SetString                uintptr
	SetNumber                uintptr
	AddCredentials           uintptr
	GetString                uintptr
	GetNumber                uintptr
	GetOptionCount           uintptr
	GetOptionAt              uintptr
	GetOption                uintptr
	GetCredentialsCount      uintptr
	GetCredentialsAt         uintptr
	GetCredentialsPasswordAt uintptr
	Clone                    uintptr
	SetInterval              uintptr
	GetInterval              uintptr
}

// ApplicationInitialize initializes the MI [Application].
// It is recommended to have only one Application per process.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_initializev1
func ApplicationInitialize() (*Application, error) {
	application := &Application{}

	r0, _, err := procMIApplicationInitialize.Call(
		0,
		uintptr(unsafe.Pointer(applicationID)),
		0,
		uintptr(unsafe.Pointer(application)),
	)
	if !errors.Is(err, windows.NOERROR) {
		return nil, fmt.Errorf("syscall returned: %w", err)
	}

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return nil, result
	}

	return application, nil
}

// Close deinitializes the management infrastructure client API that was initialized through a call to ApplicationInitialize.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_close
func (application *Application) Close() error {
	if application == nil || application.ft == nil {
		return ErrNotInitialized
	}

	r0, _, _ := syscall.SyscallN(application.ft.Close, uintptr(unsafe.Pointer(application)))

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return result
	}

	return nil
}

// NewSession creates a session used to share connections for a set of operations to a single destination.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_newsession
func (application *Application) NewSession(options *DestinationOptions) (*Session, error) {
	if application == nil || application.ft == nil {
		return nil, ErrNotInitialized
	}

	session := &Session{}

	r0, _, _ := syscall.SyscallN(
		application.ft.NewSession,
		uintptr(unsafe.Pointer(application)),
		0,
		0,
		uintptr(unsafe.Pointer(options)),
		0,
		0,
		uintptr(unsafe.Pointer(session)),
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return nil, result
	}

	defaultOperationOptions, err := application.NewOperationOptions()
	if err != nil {
		return nil, fmt.Errorf("failed to create default operation options: %w", err)
	}

	if err = defaultOperationOptions.SetTimeout(5 * time.Second); err != nil {
		return nil, fmt.Errorf("failed to set timeout: %w", err)
	}

	session.defaultOperationOptions = defaultOperationOptions

	return session, nil
}

// NewOperationOptions creates an OperationOptions object that can be used with the operation functions on the Session object.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_newoperationoptions
func (application *Application) NewOperationOptions() (*OperationOptions, error) {
	if application == nil || application.ft == nil {
		return nil, ErrNotInitialized
	}

	operationOptions := &OperationOptions{}
	mustUnderstand := True

	r0, _, _ := syscall.SyscallN(
		application.ft.NewOperationOptions,
		uintptr(unsafe.Pointer(application)),
		uintptr(mustUnderstand),
		uintptr(unsafe.Pointer(operationOptions)),
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return nil, result
	}

	return operationOptions, nil
}

// MustNewOperationOptions is the panicking version of NewOperationOptions.
func (application *Application) MustNewOperationOptions() *OperationOptions {
	operationOptions, err := application.NewOperationOptions()
	if err != nil {
		panic(fmt.Sprintf("failed to create operation options: %v", err))
	}

	return operationOptions
}

// NewDestinationOptions creates an DestinationOptions object that can be used with the Application.NewSession function.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_newdestinationoptions
func (application *Application) NewDestinationOptions() (*DestinationOptions, error) {
	if application == nil || application.ft == nil {
		return nil, ErrNotInitialized
	}

	operationOptions := &DestinationOptions{}

	r0, _, _ := syscall.SyscallN(
		application.ft.NewDestinationOptions,
		uintptr(unsafe.Pointer(application)),
		uintptr(unsafe.Pointer(operationOptions)),
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return nil, result
	}

	return operationOptions, nil
}

// SetTimeout sets the timeout for the destination options.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_destinationoptions_settimeout
func (do *DestinationOptions) SetTimeout(timeout time.Duration) error {
	if do == nil || do.ft == nil {
		return ErrNotInitialized
	}

	r0, _, _ := syscall.SyscallN(
		do.ft.SetInterval,
		uintptr(unsafe.Pointer(do)),
		uintptr(unsafe.Pointer(destinationOptionsTimeout)),
		uintptr(unsafe.Pointer(NewInterval(timeout))),
		0,
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return result
	}

	return nil
}

// SetLocale sets the locale for the destination options.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_destinationoptions_setuilocale
func (do *DestinationOptions) SetLocale(locale string) error {
	if do == nil || do.ft == nil {
		return ErrNotInitialized
	}

	localeUTF16, err := windows.UTF16PtrFromString(locale)
	if err != nil {
		return fmt.Errorf("failed to convert locale: %w", err)
	}

	r0, _, _ := syscall.SyscallN(
		do.ft.SetString,
		uintptr(unsafe.Pointer(do)),
		uintptr(unsafe.Pointer(destinationOptionsUILocale)),
		uintptr(unsafe.Pointer(localeUTF16)),
		0,
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return result
	}

	return nil
}

func (do *DestinationOptions) Delete() error {
	r0, _, _ := syscall.SyscallN(
		do.ft.Delete,
		uintptr(unsafe.Pointer(do)),
	)

	if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
		return result
	}

	return nil
}
