package api

import (
	"fmt"
	"time"

	"github.com/antitbone/dwarfctl/internal/transport"
	pb "github.com/antitbone/dwarfctl/proto"
	"google.golang.org/protobuf/proto"
)

// Telescope wraps the WebSocket transport with typed telescope commands.
type Telescope struct {
	t *transport.Client
}

// New creates a Telescope API client.
func New(clientID string) *Telescope {
	return &Telescope{t: transport.NewClient(clientID, 1)}
}

// SetDebug enables verbose WebSocket traffic logging.
func (t *Telescope) SetDebug(on bool) {
	t.t.Debug = on
}

// Connect opens a WebSocket connection to the telescope.
func (t *Telescope) Connect(ip string) error {
	return t.t.Connect(ip)
}

// Close disconnects from the telescope.
func (t *Telescope) Close() error {
	return t.t.Close()
}

// Notifications returns a channel for receiving NOTIFY packets.
func (t *Telescope) Notifications() chan *pb.WsPacket {
	return t.t.SubscribeNotifications()
}

// send marshals an inner proto message, sends it, and returns the raw response.
func (t *Telescope) send(cmd uint32, msg proto.Message) (*pb.WsPacket, error) {
	data, err := proto.Marshal(msg)
	if err != nil {
		return nil, fmt.Errorf("marshal: %w", err)
	}
	return t.t.Send(ModuleIDForCmd(cmd), cmd, data, 10*time.Second)
}

// sendEmpty sends a command with no payload (empty data field).
func (t *Telescope) sendEmpty(cmd uint32) (*pb.WsPacket, error) {
	return t.t.Send(ModuleIDForCmd(cmd), cmd, nil, 10*time.Second)
}

// sendNotify marshals and sends without waiting for a response.
func (t *Telescope) sendNotify(cmd uint32, msg proto.Message) error {
	data, err := proto.Marshal(msg)
	if err != nil {
		return err
	}
	return t.t.SendNotify(ModuleIDForCmd(cmd), cmd, data)
}

// decodeResponse parses the WsPacket data field into the given message type.
func decodeResponse(pkt *pb.WsPacket, msg proto.Message) error {
	if len(pkt.GetData()) == 0 {
		return nil
	}
	return proto.Unmarshal(pkt.GetData(), msg)
}

// SendRaw sends a raw command with pre-serialized data (no inner proto).
// Useful for testing with custom payloads.
func (t *Telescope) SendRaw(cmd uint32, data []byte) (*pb.WsPacket, error) {
	return t.t.Send(ModuleIDForCmd(cmd), cmd, data, 10*time.Second)
}

// --- Task Center commands ---

// SwitchShootingMode switches the telescope shooting mode.
// Mode 1 = photo, 2 = video/preview. Required to activate RTSP streaming on
// some firmwares — the camera won't stream until a shooting mode is active.
func (t *Telescope) SwitchShootingMode(mode int32) error {
	_, err := t.send(CmdTaskSwitchMode, &pb.ReqSwitchShootingMode{Mode: mode})
	return err
}

// --- Camera commands ---

// Camera identifies which camera to address.
type Camera int

const (
	CameraTele Camera = 0
	CameraWide Camera = 1
)

func cameraCmd(tele, wide uint32, cam Camera) uint32 {
	if cam == CameraWide {
		return wide
	}
	return tele
}

// OpenCamera opens the specified camera and starts RTSP streaming.
// The rtsp_encode_type=1 field is REQUIRED for the RTSP server to activate
// (confirmed from WsOpenCameraReq.java in the APK).
func (t *Telescope) OpenCamera(cam Camera) error {
	return t.sendNotify(cameraCmd(CmdTeleOpenCamera, CmdWideOpenCamera, cam), &pb.ReqOpenCamera{
		Binning:        false,
		RtspEncodeType: 1,
	})
}

// CloseCamera closes the specified camera.
func (t *Telescope) CloseCamera(cam Camera) error {
	return t.sendNotify(cameraCmd(CmdTeleCloseCamera, CmdWideCloseCamera, cam), &pb.ReqOpenCamera{})
}

// TakePhoto captures a single photograph.
func (t *Telescope) TakePhoto(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTelePhotograph, CmdWidePhotograph, cam))
	return err
}

// TakeRawPhoto captures a RAW photo.
func (t *Telescope) TakeRawPhoto(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTelePhotoRaw, CmdWidePhotoRaw, cam))
	return err
}

// StartBurst starts burst capture.
func (t *Telescope) StartBurst(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTeleBurst, CmdWideBurst, cam))
	return err
}

// StopBurst stops burst capture.
func (t *Telescope) StopBurst(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTeleStopBurst, CmdWideStopBurst, cam))
	return err
}

// StartRecord starts video recording.
func (t *Telescope) StartRecord(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTeleStartRecord, CmdWideStartRecord, cam))
	return err
}

// StopRecord stops video recording.
func (t *Telescope) StopRecord(cam Camera) error {
	_, err := t.sendEmpty(cameraCmd(CmdTeleStopRecord, CmdWideStopRecord, cam))
	return err
}

// SetExposure sets the exposure index on the camera.
func (t *Telescope) SetExposure(cam Camera, index int32) error {
	_, err := t.send(cameraCmd(CmdTeleSetExp, CmdWideSetExp, cam), &pb.ReqSetExp{Index: index})
	return err
}

// SetGain sets the gain index on the camera.
func (t *Telescope) SetGain(cam Camera, index int32) error {
	_, err := t.send(cameraCmd(CmdTeleSetGain, CmdWideSetGain, cam), &pb.ReqSetGain{Index: index})
	return err
}

// GetAllParams retrieves all camera parameters.
// NOTE: this command may not receive a type=3 reply on some firmwares.
// The params are also embedded in the GetDeviceState response.
func (t *Telescope) GetAllParams(cam Camera) (*pb.ResGetAllParams, error) {
	pkt, err := t.sendEmpty(cameraCmd(CmdTeleGetAllParams, CmdWideGetAllParams, cam))
	if err != nil {
		return nil, err
	}
	resp := &pb.ResGetAllParams{}
	return resp, decodeResponse(pkt, resp)
}

// --- Motor commands ---

// SlewJoystick sends a joystick vector for manual slewing (fire-and-forget).
func (t *Telescope) SlewJoystick(angle, length float64) error {
	return t.sendNotify(CmdMotorJoystick, &pb.ReqMotorServiceJoystick{
		VectorAngle:  angle,
		VectorLength: length,
	})
}

// SlewJoystickFixedAngle sends a fixed-angle joystick vector.
func (t *Telescope) SlewJoystickFixedAngle(angle, length float64) error {
	return t.sendNotify(CmdMotorJoystickFixedAngle, &pb.ReqMotorServiceJoystickFixedAngle{
		VectorAngle:  angle,
		VectorLength: length,
	})
}

// MotorStop stops a motor by ID (0=RA/az, 1=DEC/alt). Fire-and-forget —
// the telescope does not ack motor commands with a same-cmd response.
func (t *Telescope) MotorStop(motorID int32) error {
	return t.sendNotify(CmdMotorStop, &pb.ReqMotorStop{Id: motorID})
}

// MotorJoystickStop stops joystick movement. Fire-and-forget.
func (t *Telescope) MotorJoystickStop() error {
	return t.sendNotify(CmdMotorJoystickStop, &pb.ReqMotorServiceJoystickStop{})
}

// MotorRunTo moves a motor to a target position at a given speed.
// Fire-and-forget — verify via GetDeviceState after sending.
func (t *Telescope) MotorRunTo(id int32, endPos, speed float64) error {
	return t.sendNotify(CmdMotorRun, &pb.ReqMotorRunTo{
		Id:          id,
		EndPosition: endPos,
		Speed:       speed,
	})
}

// MotorGetPosition queries a motor's current position (cmd 14001).
// Returns code=-14520 (NEED_RESET) if motors haven't been homed.
// Note: this command ID is inferred, not in the APK's WsCmd enum, but
// confirmed working via live testing.
func (t *Telescope) MotorGetPosition(id int32) (*pb.ResMotorPosition, error) {
	pkt, err := t.send(CmdMotorGetPosition, &pb.ReqMotorGetPosition{Id: id})
	if err != nil {
		return nil, err
	}
	resp := &pb.ResMotorPosition{}
	return resp, decodeResponse(pkt, resp)
}

// MotorInit homes the altitude axis by slewing to a mechanical limit.
// The firmware detects the limit switch and resets position automatically.
// direction: 90 = up (toward sky), 270 = down (toward ground).
// timeoutSec is the maximum time to wait for the limit (typically 30-60s).
func (t *Telescope) MotorInit(direction float64, timeoutSec int) error {
	if err := t.SlewJoystick(direction, 0.5); err != nil {
		return fmt.Errorf("init slew: %w", err)
	}
	time.Sleep(time.Duration(timeoutSec) * time.Second)
	return t.SlewJoystick(0, 0)
}

// MotorReset resets a motor (finds home via limit switch).
// cmd 14003 is inferred. WARNING: causes telescope reboot — but may be
// the only way to unblock an axis stuck at a mechanical limit.
func (t *Telescope) MotorReset(id int32, direction bool) error {
	return t.sendNotify(CmdMotorReset, &pb.ReqMotorReset{Id: id, Direction: direction})
}

// GetFocusInfinityPos retrieves the user-defined infinity focus position.
func (t *Telescope) GetFocusInfinityPos() (int32, error) {
	pkt, err := t.sendEmpty(CmdFocusGetUserInfinity)
	if err != nil {
		return 0, err
	}
	resp := &pb.ReqSetUserInfinityPos{}
	if err := decodeResponse(pkt, resp); err != nil {
		return 0, err
	}
	return resp.GetPos(), nil
}

// SetFocusInfinityPos saves the current focus position as user infinity.
func (t *Telescope) SetFocusInfinityPos(pos int32) error {
	_, err := t.send(CmdFocusSetUserInfinity, &pb.ReqSetUserInfinityPos{Pos: pos})
	return err
}

// --- Astro commands ---

// StartCalibration begins star calibration. Requires lon/lat and a starry sky.
// This is the ONLY way to home motors and get absolute positioning.
func (t *Telescope) StartCalibration() error {
	_, err := t.send(CmdAstroStartCalibration, &pb.ReqStartCalibration{})
	return err
}

// StartCalibrationWithLocation begins calibration with explicit coordinates.
func (t *Telescope) StartCalibrationWithLocation(lon, lat float64) error {
	_, err := t.send(CmdAstroStartCalibration, &pb.ReqStartCalibration{Lon: lon, Lat: lat})
	return err
}

// StopCalibration cancels calibration.
func (t *Telescope) StopCalibration() error {
	_, err := t.sendEmpty(CmdAstroStopCalibration)
	return err
}

// GotoDSO slews to a deep-sky object by RA/Dec coordinates.
func (t *Telescope) GotoDSO(ra, dec float64, name string) error {
	_, err := t.send(CmdAstroStartGotoDSO, &pb.ReqGotoDSO{Ra: ra, Dec: dec, TargetName: name})
	return err
}

// GotoSolar slews to a solar system object by index.
func (t *Telescope) GotoSolar(index int32) error {
	_, err := t.send(CmdAstroStartGotoSolar, &pb.ReqGotoSolarSystem{Index: index})
	return err
}

// StopGoto aborts a GoTo operation.
func (t *Telescope) StopGoto() error {
	_, err := t.sendEmpty(CmdAstroStopGoto)
	return err
}

// StartLiveStacking begins live-stacking capture.
func (t *Telescope) StartLiveStacking() error {
	_, err := t.sendEmpty(CmdAstroStartLiveStacking)
	return err
}

// StopLiveStacking stops live-stacking.
func (t *Telescope) StopLiveStacking() error {
	_, err := t.sendEmpty(CmdAstroStopLiveStacking)
	return err
}

// StartEqSolving begins equatorial solving (polar alignment).
func (t *Telescope) StartEqSolving() error {
	_, err := t.sendEmpty(CmdAstroStartEqSolving)
	return err
}

// StopEqSolving stops equatorial solving.
func (t *Telescope) StopEqSolving() error {
	_, err := t.sendEmpty(CmdAstroStopEqSolving)
	return err
}

// --- Focus commands ---

// AutoFocus triggers autofocus.
func (t *Telescope) AutoFocus() error {
	_, err := t.sendEmpty(CmdFocusAutoFocus)
	return err
}

// ManualSingleStepFocus moves the focuser a single step (direction 0=in, 1=out).
func (t *Telescope) ManualSingleStepFocus(direction uint32) error {
	_, err := t.send(CmdFocusManualSingle, &pb.ReqManualSingleStepFocus{Direction: direction})
	return err
}

// StartAstroAutoFocus starts astro autofocus.
func (t *Telescope) StartAstroAutoFocus() error {
	_, err := t.sendEmpty(CmdFocusStartAstroAF)
	return err
}

// StopAstroAutoFocus stops astro autofocus.
func (t *Telescope) StopAstroAutoFocus() error {
	_, err := t.sendEmpty(CmdFocusStopAstroAF)
	return err
}

// --- Track commands ---

// StartTracking starts tracking an object at the given coordinates.
func (t *Telescope) StartTracking(x, y, w, h, camID int32) error {
	_, err := t.send(CmdTrackStart, &pb.ReqStartTrack{
		X: x, Y: y, W: w, H: h, CamId: camID,
	})
	return err
}

// StopTracking stops tracking.
func (t *Telescope) StopTracking() error {
	_, err := t.sendEmpty(CmdTrackStop)
	return err
}

// --- System commands ---

// SetTime syncs the telescope clock.
func (t *Telescope) SetTime(unixSec uint64, tzOffset float64) error {
	_, err := t.send(CmdSystemSetTime, &pb.ReqSetTime{
		Timestamp:      unixSec,
		TimezoneOffset: tzOffset,
	})
	return err
}

// SetLocation sets the observing location.
func (t *Telescope) SetLocation(lat, lng, alt float64) error {
	_, err := t.send(CmdSystemSetLocation, &pb.ReqSetLocation{
		Latitude:  lat,
		Longitude: lng,
		Altitude:  alt,
		Enable:    true,
	})
	return err
}

// --- Power / RGB commands ---

// RGBOn turns on the RGB ring light. Fire-and-forget — no type=3 reply.
// Verify via GetDeviceState().DeviceStateInfo.RgbState.
func (t *Telescope) RGBOn() error {
	return t.sendNotify(CmdRGBOn, &pb.ReqMotorServiceJoystickStop{})
}

// RGBOff turns off the RGB ring light. Fire-and-forget.
func (t *Telescope) RGBOff() error {
	return t.sendNotify(CmdRGBOff, &pb.ReqMotorServiceJoystickStop{})
}

// PowerDown powers off the telescope. Fire-and-forget.
func (t *Telescope) PowerDown() error {
	return t.sendNotify(CmdPowerDown, &pb.ReqMotorServiceJoystickStop{})
}

// Reboot reboots the telescope. Fire-and-forget.
func (t *Telescope) Reboot() error {
	return t.sendNotify(CmdReboot, &pb.ReqMotorServiceJoystickStop{})
}

// --- Task Center ---

// GetDeviceState queries the current device state.
func (t *Telescope) GetDeviceState() (*pb.ResGetDeviceStateInfo, error) {
	pkt, err := t.sendEmpty(CmdTaskGetDeviceState)
	if err != nil {
		return nil, err
	}
	resp := &pb.ResGetDeviceStateInfo{}
	return resp, decodeResponse(pkt, resp)
}

// --- Panorama commands ---

// StartPanoramaGrid starts a panorama grid scan (triggers motor home/reset).
func (t *Telescope) StartPanoramaGrid() error {
	return t.sendNotify(CmdPanoStartGrid, &pb.ReqStartPanoramaByGrid{})
}

// StopPanorama stops the current panorama operation.
func (t *Telescope) StopPanorama() error {
	return t.sendNotify(CmdPanoStop, &pb.ReqStopPanorama{})
}
