package main

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/0x524a/onvif-go"
)

const version = "1.0.0"

type CameraReport struct {
	Timestamp       string                  `json:"timestamp"`
	UtilityVersion  string                  `json:"utility_version"`
	ConnectionInfo  ConnectionInfo          `json:"connection_info"`
	DeviceInfo      *DeviceInfoResult       `json:"device_info"`
	Capabilities    *CapabilitiesResult     `json:"capabilities"`
	Profiles        *ProfilesResult         `json:"profiles"`
	StreamURIs      []StreamURIResult       `json:"stream_uris"`
	SnapshotURIs    []SnapshotURIResult     `json:"snapshot_uris"`
	VideoEncoders   []VideoEncoderResult    `json:"video_encoders"`
	ImagingSettings []ImagingSettingsResult `json:"imaging_settings"`
	PTZStatus       []PTZStatusResult       `json:"ptz_status"`
	PTZPresets      []PTZPresetsResult      `json:"ptz_presets"`
	SystemDateTime  *SystemDateTimeResult   `json:"system_datetime"`
	RawResponses    map[string]interface{}  `json:"raw_responses,omitempty"`
	Errors          []ErrorLog              `json:"errors"`
}

type ConnectionInfo struct {
	Endpoint string `json:"endpoint"`
	Username string `json:"username"`
	TestDate string `json:"test_date"`
}

type DeviceInfoResult struct {
	Success      bool                     `json:"success"`
	Data         *onvif.DeviceInformation `json:"data,omitempty"`
	Error        string                   `json:"error,omitempty"`
	ResponseTime string                   `json:"response_time"`
}

type CapabilitiesResult struct {
	Success      bool                `json:"success"`
	Data         *onvif.Capabilities `json:"data,omitempty"`
	Error        string              `json:"error,omitempty"`
	ResponseTime string              `json:"response_time"`
}

type ProfilesResult struct {
	Success      bool             `json:"success"`
	Data         []*onvif.Profile `json:"data,omitempty"`
	Count        int              `json:"count"`
	Error        string           `json:"error,omitempty"`
	ResponseTime string           `json:"response_time"`
}

type StreamURIResult struct {
	ProfileToken string          `json:"profile_token"`
	ProfileName  string          `json:"profile_name"`
	Success      bool            `json:"success"`
	Data         *onvif.MediaURI `json:"data,omitempty"`
	Error        string          `json:"error,omitempty"`
	ResponseTime string          `json:"response_time"`
}

type SnapshotURIResult struct {
	ProfileToken string          `json:"profile_token"`
	ProfileName  string          `json:"profile_name"`
	Success      bool            `json:"success"`
	Data         *onvif.MediaURI `json:"data,omitempty"`
	Error        string          `json:"error,omitempty"`
	ResponseTime string          `json:"response_time"`
}

type VideoEncoderResult struct {
	ProfileToken string                           `json:"profile_token"`
	ProfileName  string                           `json:"profile_name"`
	Success      bool                             `json:"success"`
	Data         *onvif.VideoEncoderConfiguration `json:"data,omitempty"`
	Error        string                           `json:"error,omitempty"`
	ResponseTime string                           `json:"response_time"`
}

type ImagingSettingsResult struct {
	VideoSourceToken string                 `json:"video_source_token"`
	Success          bool                   `json:"success"`
	Data             *onvif.ImagingSettings `json:"data,omitempty"`
	Error            string                 `json:"error,omitempty"`
	ResponseTime     string                 `json:"response_time"`
}

type PTZStatusResult struct {
	ProfileToken string           `json:"profile_token"`
	ProfileName  string           `json:"profile_name"`
	Success      bool             `json:"success"`
	Data         *onvif.PTZStatus `json:"data,omitempty"`
	Error        string           `json:"error,omitempty"`
	ResponseTime string           `json:"response_time"`
}

type PTZPresetsResult struct {
	ProfileToken string             `json:"profile_token"`
	ProfileName  string             `json:"profile_name"`
	Success      bool               `json:"success"`
	Data         []*onvif.PTZPreset `json:"data,omitempty"`
	Count        int                `json:"count"`
	Error        string             `json:"error,omitempty"`
	ResponseTime string             `json:"response_time"`
}

type SystemDateTimeResult struct {
	Success      bool        `json:"success"`
	Data         interface{} `json:"data,omitempty"`
	Error        string      `json:"error,omitempty"`
	ResponseTime string      `json:"response_time"`
}

type ErrorLog struct {
	Operation string `json:"operation"`
	Error     string `json:"error"`
	Timestamp string `json:"timestamp"`
}

var (
	endpoint   = flag.String("endpoint", "", "ONVIF device endpoint (e.g., http://192.168.1.201/onvif/device_service)")
	username   = flag.String("username", "", "ONVIF username")
	password   = flag.String("password", "", "ONVIF password")
	outputDir  = flag.String("output", "./camera-logs", "Output directory for logs")
	timeout    = flag.Int("timeout", 30, "Request timeout in seconds")
	verbose    = flag.Bool("verbose", false, "Verbose output")
	captureXML = flag.Bool("capture-xml", false, "Capture raw SOAP XML traffic and create tar.gz archive")
)

func main() {
	flag.Parse()

	fmt.Printf("ONVIF Camera Diagnostic Utility v%s\n", version)
	fmt.Println("========================================")
	fmt.Println()

	// Validate inputs
	if *endpoint == "" || *username == "" || *password == "" {
		fmt.Println("Error: Missing required parameters")
		fmt.Println()
		fmt.Println("Usage:")
		flag.PrintDefaults()
		fmt.Println()
		fmt.Println("Example:")
		fmt.Println("  ./onvif-diagnostics -endpoint http://192.168.1.201/onvif/device_service -username service -password Service.1234")
		os.Exit(1)
	}

	// Create output directory
	if err := os.MkdirAll(*outputDir, 0755); err != nil {
		log.Fatalf("Failed to create output directory: %v", err)
	}

	// Initialize report
	report := &CameraReport{
		Timestamp:      time.Now().Format(time.RFC3339),
		UtilityVersion: version,
		ConnectionInfo: ConnectionInfo{
			Endpoint: *endpoint,
			Username: *username,
			TestDate: time.Now().Format("2006-01-02"),
		},
		Errors:       make([]ErrorLog, 0),
		RawResponses: make(map[string]interface{}),
	}

	// Setup XML capture if requested
	var loggingTransport *LoggingTransport
	var xmlCaptureDir string

	if *captureXML {
		timestamp := time.Now().Format("20060102-150405")
		xmlCaptureDir = filepath.Join(*outputDir, "temp_"+timestamp)
		if err := os.MkdirAll(xmlCaptureDir, 0755); err != nil {
			log.Fatalf("Failed to create XML capture directory: %v", err)
		}

		loggingTransport = &LoggingTransport{
			Transport: &http.Transport{
				MaxIdleConns:        10,
				MaxIdleConnsPerHost: 5,
				IdleConnTimeout:     90 * time.Second,
			},
			LogDir:  xmlCaptureDir,
			Counter: 0,
		}

		if *verbose {
			fmt.Printf("📦 XML capture enabled, saving to: %s\n", xmlCaptureDir)
		}
	}

	// Create ONVIF client
	var client *onvif.Client
	var err error

	if loggingTransport != nil {
		httpClient := &http.Client{
			Timeout:   time.Duration(*timeout) * time.Second,
			Transport: loggingTransport,
		}
		client, err = onvif.NewClient(
			*endpoint,
			onvif.WithCredentials(*username, *password),
			onvif.WithHTTPClient(httpClient),
		)
	} else {
		client, err = onvif.NewClient(
			*endpoint,
			onvif.WithCredentials(*username, *password),
			onvif.WithTimeout(time.Duration(*timeout)*time.Second),
		)
	}

	if err != nil {
		log.Fatalf("Failed to create ONVIF client: %v", err)
	}

	ctx := context.Background()

	fmt.Println("Starting diagnostic collection...")
	fmt.Println()

	// Test 1: Get Device Information
	logStep("1. Getting device information...")
	report.DeviceInfo = testGetDeviceInformation(ctx, client, report)

	// Test 2: Get System Date and Time
	logStep("2. Getting system date and time...")
	report.SystemDateTime = testGetSystemDateTime(ctx, client, report)

	// Test 3: Get Capabilities
	logStep("3. Getting capabilities...")
	report.Capabilities = testGetCapabilities(ctx, client, report)

	// Test 4: Initialize (discover services)
	logStep("4. Discovering service endpoints...")
	if err := client.Initialize(ctx); err != nil {
		logError("Service discovery failed: %v", err)
		report.Errors = append(report.Errors, ErrorLog{
			Operation: "Initialize",
			Error:     err.Error(),
			Timestamp: time.Now().Format(time.RFC3339),
		})
	} else {
		logSuccess("Service endpoints discovered")
	}

	// Test 5: Get Profiles
	logStep("5. Getting media profiles...")
	report.Profiles = testGetProfiles(ctx, client, report)

	// Test 6: Get Stream URIs (for each profile)
	if report.Profiles != nil && report.Profiles.Success {
		logStep("6. Getting stream URIs for all profiles...")
		report.StreamURIs = testGetStreamURIs(ctx, client, report.Profiles.Data, report)
	}

	// Test 7: Get Snapshot URIs (for each profile)
	if report.Profiles != nil && report.Profiles.Success {
		logStep("7. Getting snapshot URIs for all profiles...")
		report.SnapshotURIs = testGetSnapshotURIs(ctx, client, report.Profiles.Data, report)
	}

	// Test 8: Get Video Encoder Configurations
	if report.Profiles != nil && report.Profiles.Success {
		logStep("8. Getting video encoder configurations...")
		report.VideoEncoders = testGetVideoEncoders(ctx, client, report.Profiles.Data, report)
	}

	// Test 9: Get Imaging Settings
	if report.Profiles != nil && report.Profiles.Success {
		logStep("9. Getting imaging settings...")
		report.ImagingSettings = testGetImagingSettings(ctx, client, report.Profiles.Data, report)
	}

	// Test 10: Get PTZ Status (if PTZ is available)
	if report.Profiles != nil && report.Profiles.Success {
		logStep("10. Getting PTZ status...")
		report.PTZStatus = testGetPTZStatus(ctx, client, report.Profiles.Data, report)
	}

	// Test 11: Get PTZ Presets (if PTZ is available)
	if report.Profiles != nil && report.Profiles.Success {
		logStep("11. Getting PTZ presets...")
		report.PTZPresets = testGetPTZPresets(ctx, client, report.Profiles.Data, report)
	}

	// Generate output filename based on device info
	filename := generateFilename(report)
	outputPath := filepath.Join(*outputDir, filename)

	// Save report
	logStep("Saving diagnostic report...")
	if err := saveReport(report, outputPath); err != nil {
		log.Fatalf("Failed to save report: %v", err)
	}

	// Create XML archive if capture was enabled
	if *captureXML && loggingTransport != nil {
		fmt.Println()
		logStep("Creating XML capture archive...")

		// Generate archive name based on device info
		var archiveName string
		if report.DeviceInfo != nil && report.DeviceInfo.Success {
			manufacturer := sanitizeFilename(report.DeviceInfo.Data.Manufacturer)
			model := sanitizeFilename(report.DeviceInfo.Data.Model)
			firmware := sanitizeFilename(report.DeviceInfo.Data.FirmwareVersion)
			timestamp := time.Now().Format("20060102-150405")
			archiveName = fmt.Sprintf("%s_%s_%s_xmlcapture_%s.tar.gz", manufacturer, model, firmware, timestamp)
		} else {
			timestamp := time.Now().Format("20060102-150405")
			archiveName = fmt.Sprintf("unknown_device_xmlcapture_%s.tar.gz", timestamp)
		}

		archivePath := filepath.Join(*outputDir, archiveName)

		if err := createTarGz(xmlCaptureDir, archivePath); err != nil {
			logError("Failed to create XML archive: %v", err)
		} else {
			logSuccess("XML archive created: %s", archiveName)
			logSuccess("Total SOAP calls captured: %d", loggingTransport.Counter)

			// Remove temporary directory
			if err := os.RemoveAll(xmlCaptureDir); err != nil {
				logError("Warning: Failed to remove temp directory: %v", err)
			}
		}
	}

	fmt.Println()
	fmt.Println("========================================")
	fmt.Printf("✓ Diagnostic collection complete!\n")
	fmt.Printf("  Report saved to: %s\n", outputPath)
	fmt.Printf("  Total errors: %d\n", len(report.Errors))

	if report.DeviceInfo != nil && report.DeviceInfo.Success {
		fmt.Printf("\n  Device: %s %s\n", report.DeviceInfo.Data.Manufacturer, report.DeviceInfo.Data.Model)
		fmt.Printf("  Firmware: %s\n", report.DeviceInfo.Data.FirmwareVersion)
	}

	if report.Profiles != nil && report.Profiles.Success {
		fmt.Printf("  Profiles: %d\n", report.Profiles.Count)
	}

	fmt.Println()
	if *captureXML {
		fmt.Println("Both JSON report and XML capture archive saved to camera-logs/")
		fmt.Println("Share both files for comprehensive analysis.")
	} else {
		fmt.Println("Use -capture-xml flag to also capture raw SOAP XML traffic.")
		fmt.Println("Please share this file for analysis and test creation.")
	}
	fmt.Println("========================================")
}

func testGetDeviceInformation(ctx context.Context, client *onvif.Client, report *CameraReport) *DeviceInfoResult {
	start := time.Now()
	result := &DeviceInfoResult{}

	info, err := client.GetDeviceInformation(ctx)
	result.ResponseTime = time.Since(start).String()

	if err != nil {
		result.Success = false
		result.Error = err.Error()
		logError("Failed: %v", err)
		report.Errors = append(report.Errors, ErrorLog{
			Operation: "GetDeviceInformation",
			Error:     err.Error(),
			Timestamp: time.Now().Format(time.RFC3339),
		})
	} else {
		result.Success = true
		result.Data = info
		logSuccess("Manufacturer: %s, Model: %s", info.Manufacturer, info.Model)
	}

	return result
}

func testGetSystemDateTime(ctx context.Context, client *onvif.Client, report *CameraReport) *SystemDateTimeResult {
	start := time.Now()
	result := &SystemDateTimeResult{}

	dateTime, err := client.GetSystemDateAndTime(ctx)
	result.ResponseTime = time.Since(start).String()

	if err != nil {
		result.Success = false
		result.Error = err.Error()
		logError("Failed: %v", err)
		report.Errors = append(report.Errors, ErrorLog{
			Operation: "GetSystemDateAndTime",
			Error:     err.Error(),
			Timestamp: time.Now().Format(time.RFC3339),
		})
	} else {
		result.Success = true
		result.Data = dateTime
		logSuccess("Retrieved")
	}

	return result
}

func testGetCapabilities(ctx context.Context, client *onvif.Client, report *CameraReport) *CapabilitiesResult {
	start := time.Now()
	result := &CapabilitiesResult{}

	capabilities, err := client.GetCapabilities(ctx)
	result.ResponseTime = time.Since(start).String()

	if err != nil {
		result.Success = false
		result.Error = err.Error()
		logError("Failed: %v", err)
		report.Errors = append(report.Errors, ErrorLog{
			Operation: "GetCapabilities",
			Error:     err.Error(),
			Timestamp: time.Now().Format(time.RFC3339),
		})
	} else {
		result.Success = true
		result.Data = capabilities

		services := []string{}
		if capabilities.Device != nil {
			services = append(services, "Device")
		}
		if capabilities.Media != nil {
			services = append(services, "Media")
		}
		if capabilities.PTZ != nil {
			services = append(services, "PTZ")
		}
		if capabilities.Imaging != nil {
			services = append(services, "Imaging")
		}
		if capabilities.Events != nil {
			services = append(services, "Events")
		}
		if capabilities.Analytics != nil {
			services = append(services, "Analytics")
		}

		logSuccess("Services: %s", strings.Join(services, ", "))
	}

	return result
}

func testGetProfiles(ctx context.Context, client *onvif.Client, report *CameraReport) *ProfilesResult {
	start := time.Now()
	result := &ProfilesResult{}

	profiles, err := client.GetProfiles(ctx)
	result.ResponseTime = time.Since(start).String()

	if err != nil {
		result.Success = false
		result.Error = err.Error()
		logError("Failed: %v", err)
		report.Errors = append(report.Errors, ErrorLog{
			Operation: "GetProfiles",
			Error:     err.Error(),
			Timestamp: time.Now().Format(time.RFC3339),
		})
	} else {
		result.Success = true
		result.Data = profiles
		result.Count = len(profiles)
		logSuccess("Found %d profile(s)", len(profiles))

		for i, profile := range profiles {
			if *verbose {
				fmt.Printf("   Profile %d: %s (Token: %s)\n", i+1, profile.Name, profile.Token)
				if profile.VideoEncoderConfiguration != nil && profile.VideoEncoderConfiguration.Resolution != nil {
					fmt.Printf("     Resolution: %dx%d, Encoding: %s\n",
						profile.VideoEncoderConfiguration.Resolution.Width,
						profile.VideoEncoderConfiguration.Resolution.Height,
						profile.VideoEncoderConfiguration.Encoding)
				}
			}
		}
	}

	return result
}

func testGetStreamURIs(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []StreamURIResult {
	results := make([]StreamURIResult, 0)

	for _, profile := range profiles {
		start := time.Now()
		result := StreamURIResult{
			ProfileToken: profile.Token,
			ProfileName:  profile.Name,
		}

		streamURI, err := client.GetStreamURI(ctx, profile.Token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Profile %s: %v", profile.Name, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetStreamURI[%s]", profile.Token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = streamURI
			if *verbose {
				logSuccess("  Profile %s: %s", profile.Name, streamURI.URI)
			}
		}

		results = append(results, result)
	}

	successCount := 0
	for _, r := range results {
		if r.Success {
			successCount++
		}
	}
	logSuccess("Retrieved %d/%d stream URIs", successCount, len(results))

	return results
}

func testGetSnapshotURIs(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []SnapshotURIResult {
	results := make([]SnapshotURIResult, 0)

	for _, profile := range profiles {
		start := time.Now()
		result := SnapshotURIResult{
			ProfileToken: profile.Token,
			ProfileName:  profile.Name,
		}

		snapshotURI, err := client.GetSnapshotURI(ctx, profile.Token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Profile %s: %v", profile.Name, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetSnapshotURI[%s]", profile.Token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = snapshotURI
			if *verbose {
				logSuccess("  Profile %s: %s", profile.Name, snapshotURI.URI)
			}
		}

		results = append(results, result)
	}

	successCount := 0
	for _, r := range results {
		if r.Success {
			successCount++
		}
	}
	logSuccess("Retrieved %d/%d snapshot URIs", successCount, len(results))

	return results
}

func testGetVideoEncoders(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []VideoEncoderResult {
	results := make([]VideoEncoderResult, 0)

	for _, profile := range profiles {
		if profile.VideoEncoderConfiguration == nil {
			continue
		}

		start := time.Now()
		result := VideoEncoderResult{
			ProfileToken: profile.Token,
			ProfileName:  profile.Name,
		}

		config, err := client.GetVideoEncoderConfiguration(ctx, profile.VideoEncoderConfiguration.Token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Profile %s: %v", profile.Name, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetVideoEncoderConfiguration[%s]", profile.Token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = config
			if *verbose && config.Resolution != nil && config.RateControl != nil {
				logSuccess("  Profile %s: %s %dx%d @ %dfps",
					profile.Name, config.Encoding,
					config.Resolution.Width, config.Resolution.Height,
					config.RateControl.FrameRateLimit)
			}
		}

		results = append(results, result)
	}

	successCount := 0
	for _, r := range results {
		if r.Success {
			successCount++
		}
	}
	logSuccess("Retrieved %d/%d video encoder configs", successCount, len(results))

	return results
}

func testGetImagingSettings(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []ImagingSettingsResult {
	results := make([]ImagingSettingsResult, 0)
	processed := make(map[string]bool)

	for _, profile := range profiles {
		if profile.VideoSourceConfiguration == nil {
			continue
		}

		token := profile.VideoSourceConfiguration.SourceToken
		if processed[token] {
			continue
		}
		processed[token] = true

		start := time.Now()
		result := ImagingSettingsResult{
			VideoSourceToken: token,
		}

		settings, err := client.GetImagingSettings(ctx, token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Video source %s: %v", token, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetImagingSettings[%s]", token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = settings
			if *verbose {
				fmt.Printf("   ✓ Video source %s: Retrieved\n", token)
			}
		}

		results = append(results, result)
	}

	successCount := 0
	for _, r := range results {
		if r.Success {
			successCount++
		}
	}
	logSuccess("Retrieved %d/%d imaging settings", successCount, len(results))

	return results
}

func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []PTZStatusResult {
	results := make([]PTZStatusResult, 0)

	for _, profile := range profiles {
		if profile.PTZConfiguration == nil {
			continue
		}

		start := time.Now()
		result := PTZStatusResult{
			ProfileToken: profile.Token,
			ProfileName:  profile.Name,
		}

		status, err := client.GetStatus(ctx, profile.Token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Profile %s: %v", profile.Name, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetPTZStatus[%s]", profile.Token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = status
			if *verbose {
				logSuccess("  Profile %s: Retrieved", profile.Name)
			}
		}

		results = append(results, result)
	}

	if len(results) == 0 {
		logInfo("No PTZ configurations found")
	} else {
		successCount := 0
		for _, r := range results {
			if r.Success {
				successCount++
			}
		}
		logSuccess("Retrieved %d/%d PTZ status", successCount, len(results))
	}

	return results
}

func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []PTZPresetsResult {
	results := make([]PTZPresetsResult, 0)

	for _, profile := range profiles {
		if profile.PTZConfiguration == nil {
			continue
		}

		start := time.Now()
		result := PTZPresetsResult{
			ProfileToken: profile.Token,
			ProfileName:  profile.Name,
		}

		presets, err := client.GetPresets(ctx, profile.Token)
		result.ResponseTime = time.Since(start).String()

		if err != nil {
			result.Success = false
			result.Error = err.Error()
			if *verbose {
				logError("  Profile %s: %v", profile.Name, err)
			}
			report.Errors = append(report.Errors, ErrorLog{
				Operation: fmt.Sprintf("GetPTZPresets[%s]", profile.Token),
				Error:     err.Error(),
				Timestamp: time.Now().Format(time.RFC3339),
			})
		} else {
			result.Success = true
			result.Data = presets
			result.Count = len(presets)
			if *verbose {
				logSuccess("  Profile %s: %d preset(s)", profile.Name, len(presets))
			}
		}

		results = append(results, result)
	}

	if len(results) == 0 {
		logInfo("No PTZ configurations found")
	} else {
		successCount := 0
		totalPresets := 0
		for _, r := range results {
			if r.Success {
				successCount++
				totalPresets += r.Count
			}
		}
		logSuccess("Retrieved presets from %d/%d PTZ profiles (%d total presets)", successCount, len(results), totalPresets)
	}

	return results
}

func generateFilename(report *CameraReport) string {
	timestamp := time.Now().Format("20060102-150405")

	if report.DeviceInfo != nil && report.DeviceInfo.Success {
		manufacturer := sanitizeFilename(report.DeviceInfo.Data.Manufacturer)
		model := sanitizeFilename(report.DeviceInfo.Data.Model)
		firmware := sanitizeFilename(report.DeviceInfo.Data.FirmwareVersion)

		return fmt.Sprintf("%s_%s_%s_%s.json", manufacturer, model, firmware, timestamp)
	}

	return fmt.Sprintf("unknown_camera_%s.json", timestamp)
}

func sanitizeFilename(s string) string {
	s = strings.ReplaceAll(s, " ", "_")
	s = strings.ReplaceAll(s, "/", "-")
	s = strings.ReplaceAll(s, "\\", "-")
	s = strings.ReplaceAll(s, ":", "-")
	s = strings.ReplaceAll(s, "*", "-")
	s = strings.ReplaceAll(s, "?", "-")
	s = strings.ReplaceAll(s, "\"", "-")
	s = strings.ReplaceAll(s, "<", "-")
	s = strings.ReplaceAll(s, ">", "-")
	s = strings.ReplaceAll(s, "|", "-")
	return s
}

func saveReport(report *CameraReport, filename string) error {
	data, err := json.MarshalIndent(report, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal report: %w", err)
	}

	if err := os.WriteFile(filename, data, 0644); err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}

	return nil
}

func logStep(format string, args ...interface{}) {
	fmt.Printf("→ "+format+"\n", args...)
}

func logSuccess(format string, args ...interface{}) {
	fmt.Printf("  ✓ "+format+"\n", args...)
}

func logError(format string, args ...interface{}) {
	fmt.Printf("  ✗ "+format+"\n", args...)
}

func logInfo(format string, args ...interface{}) {
	fmt.Printf("  ℹ "+format+"\n", args...)
}

// XML Capture functionality

// XMLCapture stores a request/response pair
type XMLCapture struct {
	Timestamp     string `json:"timestamp"`
	Operation     int    `json:"operation"`
	OperationName string `json:"operation_name"`
	Endpoint      string `json:"endpoint"`
	RequestBody   string `json:"request_body"`
	ResponseBody  string `json:"response_body"`
	StatusCode    int    `json:"status_code"`
	Error         string `json:"error,omitempty"`
}

// LoggingTransport wraps http.RoundTripper to log requests and responses
type LoggingTransport struct {
	Transport http.RoundTripper
	LogDir    string
	Counter   int
}

func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	t.Counter++
	capture := XMLCapture{
		Timestamp: time.Now().Format(time.RFC3339),
		Operation: t.Counter,
		Endpoint:  req.URL.String(),
	}

	// Capture request body
	if req.Body != nil {
		bodyBytes, err := io.ReadAll(req.Body)
		if err == nil {
			capture.RequestBody = string(bodyBytes)
			// Extract operation name from SOAP body
			capture.OperationName = extractSOAPOperation(capture.RequestBody)
			// Restore the body for the actual request
			req.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
		}
	}

	// Make the actual request
	resp, err := t.Transport.RoundTrip(req)
	if err != nil {
		capture.Error = err.Error()
		t.saveCapture(capture)
		return nil, err
	}

	// Capture response
	capture.StatusCode = resp.StatusCode
	if resp.Body != nil {
		bodyBytes, err := io.ReadAll(resp.Body)
		if err == nil {
			capture.ResponseBody = string(bodyBytes)
			// Restore the body for the caller
			resp.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
		}
	}

	t.saveCapture(capture)
	return resp, nil
}

// prettyPrintXML formats XML with proper indentation using a simple algorithm
func prettyPrintXML(xmlStr string) string {
	if xmlStr == "" {
		return ""
	}

	var formatted bytes.Buffer
	decoder := xml.NewDecoder(strings.NewReader(xmlStr))
	encoder := xml.NewEncoder(&formatted)
	encoder.Indent("", "  ")

	for {
		token, err := decoder.Token()
		if err != nil {
			if err.Error() == "EOF" {
				break
			}
			// If formatting fails, return original
			return xmlStr
		}

		if err := encoder.EncodeToken(token); err != nil {
			return xmlStr
		}
	}

	if err := encoder.Flush(); err != nil {
		return xmlStr
	}

	return formatted.String()
}

func (t *LoggingTransport) saveCapture(capture XMLCapture) {
	// Create filename base using operation name
	baseFilename := fmt.Sprintf("capture_%03d_%s", capture.Operation, capture.OperationName)

	// Save as individual JSON file
	filename := filepath.Join(t.LogDir, baseFilename+".json")
	data, err := json.MarshalIndent(capture, "", "  ")
	if err != nil {
		log.Printf("Failed to marshal capture: %v", err)
		return
	}

	if err := os.WriteFile(filename, data, 0644); err != nil {
		log.Printf("Failed to write capture: %v", err)
	}

	// Pretty-print and save XML files for easier viewing
	reqFile := filepath.Join(t.LogDir, baseFilename+"_request.xml")
	prettyRequest := prettyPrintXML(capture.RequestBody)
	if err := os.WriteFile(reqFile, []byte(prettyRequest), 0644); err != nil {
		log.Printf("Failed to write request XML: %v", err)
	}

	respFile := filepath.Join(t.LogDir, baseFilename+"_response.xml")
	prettyResponse := prettyPrintXML(capture.ResponseBody)
	if err := os.WriteFile(respFile, []byte(prettyResponse), 0644); err != nil {
		log.Printf("Failed to write response XML: %v", err)
	}
}

// extractSOAPOperation extracts the operation name from a SOAP request body
func extractSOAPOperation(soapBody string) string {
	// Look for the operation element in the SOAP Body
	// Typical format: <Body><GetDeviceInformation xmlns="...">...</GetDeviceInformation></Body>

	// Find the Body element
	bodyStart := strings.Index(soapBody, "<Body")
	if bodyStart == -1 {
		return "Unknown"
	}

	// Find the closing > of the Body opening tag
	bodyOpenEnd := strings.Index(soapBody[bodyStart:], ">")
	if bodyOpenEnd == -1 {
		return "Unknown"
	}
	bodyContentStart := bodyStart + bodyOpenEnd + 1

	// Find the first element after <Body>
	// Skip whitespace and find next <
	for bodyContentStart < len(soapBody) && soapBody[bodyContentStart] <= ' ' {
		bodyContentStart++
	}

	if bodyContentStart >= len(soapBody) || soapBody[bodyContentStart] != '<' {
		return "Unknown"
	}

	// Extract the tag name
	tagStart := bodyContentStart + 1
	tagEnd := tagStart
	for tagEnd < len(soapBody) && soapBody[tagEnd] != ' ' && soapBody[tagEnd] != '>' && soapBody[tagEnd] != '/' {
		tagEnd++
	}

	if tagEnd > tagStart {
		tagName := soapBody[tagStart:tagEnd]
		// Remove namespace prefix if present (e.g., "tds:GetDeviceInformation" -> "GetDeviceInformation")
		if colonIdx := strings.Index(tagName, ":"); colonIdx != -1 {
			return tagName[colonIdx+1:]
		}
		return tagName
	}

	return "Unknown"
}

// createTarGz creates a tar.gz archive from a directory
func createTarGz(sourceDir, archivePath string) error {
	// Create archive file
	archiveFile, err := os.Create(archivePath)
	if err != nil {
		return fmt.Errorf("failed to create archive file: %w", err)
	}
	defer archiveFile.Close()

	// Create gzip writer
	gzWriter := gzip.NewWriter(archiveFile)
	defer gzWriter.Close()

	// Create tar writer
	tarWriter := tar.NewWriter(gzWriter)
	defer tarWriter.Close()

	// Walk through source directory
	return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Skip the root directory itself
		if path == sourceDir {
			return nil
		}

		// Create tar header
		header, err := tar.FileInfoHeader(info, "")
		if err != nil {
			return fmt.Errorf("failed to create tar header: %w", err)
		}

		// Set name to relative path
		relPath, err := filepath.Rel(sourceDir, path)
		if err != nil {
			return fmt.Errorf("failed to get relative path: %w", err)
		}
		header.Name = relPath

		// Write header
		if err := tarWriter.WriteHeader(header); err != nil {
			return fmt.Errorf("failed to write tar header: %w", err)
		}

		// If it's a file, write its content
		if !info.IsDir() {
			file, err := os.Open(path)
			if err != nil {
				return fmt.Errorf("failed to open file: %w", err)
			}
			defer file.Close()

			if _, err := io.Copy(tarWriter, file); err != nil {
				return fmt.Errorf("failed to write file to tar: %w", err)
			}
		}

		return nil
	})
}
