package crashlog

import (
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/apex/log"
)

// generateIDAScripts creates IDAPython scripts for marking panic frames in IDA Pro.
// It generates separate scripts for kernel frames and DSC/user frames.
func (i *Ips) generateIDAScripts() error {
	if i.Payload.panic210 == nil {
		return fmt.Errorf("no panic data available for IDA script generation")
	}

	panicPID := i.Payload.panic210.PanickedTask.PID
	panicTID := i.Payload.panic210.PanickedThread.TID

	proc, ok := i.Payload.ProcessByPid[panicPID]
	if !ok {
		return fmt.Errorf("panicked process PID %d not found", panicPID)
	}
	thread, ok := proc.ThreadByID[panicTID]
	if !ok {
		return fmt.Errorf("panicked thread TID %d not found", panicTID)
	}

	// Build panic description for script header
	panicDesc := "Unknown panic"
	if i.Payload.panic210.Panic != "" {
		panicDesc = i.Payload.panic210.Panic
		// Truncate if too long
		if len(panicDesc) > 80 {
			panicDesc = panicDesc[:77] + "..."
		}
	}

	// Generate kernel frames script if there are kernel frames
	if len(thread.KernelFrames) > 0 {
		if err := i.generateIDAScript("kc", thread.KernelFrames, panicDesc, true); err != nil {
			return fmt.Errorf("failed to generate kernel IDA script: %w", err)
		}
	}

	// Generate DSC/user frames script if there are user frames
	if len(thread.UserFrames) > 0 {
		if err := i.generateIDAScript("dsc", thread.UserFrames, panicDesc, false); err != nil {
			return fmt.Errorf("failed to generate DSC IDA script: %w", err)
		}
	}

	return nil
}

// generateIDAScript creates an IDAPython script for the given frames.
func (i *Ips) generateIDAScript(suffix string, frames []PanicFrame, panicDesc string, isKernel bool) error {
	// Build output filename based on crashlog name
	scriptPath := fmt.Sprintf("%s.%s.ida.py", i.Config.CrashlogPath, suffix)

	// Generate a unique color based on incident ID hash (so different crashes get different colors)
	// Use a set of visually distinct, readable colors (BGR format for IDA)
	colors := []uint32{
		0x0080ff, // orange
		0xff8080, // light blue
		0x80ff80, // light green
		0xff80ff, // pink/magenta
		0x80ffff, // yellow
		0xffff80, // cyan
		0x8080ff, // salmon/coral
		0xff0080, // purple
	}
	colorIndex := 0
	if i.Header.IncidentID != "" {
		// Simple hash of incident ID to pick a color
		var hash uint32
		for _, c := range i.Header.IncidentID {
			hash = hash*31 + uint32(c)
		}
		colorIndex = int(hash % uint32(len(colors)))
	}
	panicColor := colors[colorIndex]

	// Format timestamp
	timestamp := time.Time(i.Header.Timestamp).Format("2006-01-02 15:04:05 MST")

	// escapePyString escapes a string for use in Python string literals
	escapePyString := func(s string) string {
		s = strings.ReplaceAll(s, `\`, `\\`)
		s = strings.ReplaceAll(s, `"`, `\"`)
		return s
	}

	var sb strings.Builder

	// Write script header (no shebang - IDA runs Python scripts internally)
	sb.WriteString(`"""
IDAPython script generated by ipsw symbolicate --ida
`)
	sb.WriteString(fmt.Sprintf("Panic: %s\n", panicDesc))
	sb.WriteString(fmt.Sprintf("Device: %s - %s %s (%s)\n", i.Payload.Product, i.Payload.OsVersion.Train, i.Header.Version(), i.Header.Build()))
	sb.WriteString(fmt.Sprintf("Timestamp: %s\n", timestamp))
	sb.WriteString(fmt.Sprintf("Incident ID: %s\n", i.Header.IncidentID))
	sb.WriteString(fmt.Sprintf("Crashlog: %s\n", filepath.Base(i.Config.CrashlogPath)))
	if isKernel {
		sb.WriteString("Target: Kernelcache\n")
		if len(i.KernelSymbols) > 0 {
			sb.WriteString(fmt.Sprintf("Kernel symbols: %d (from kernel analysis)\n", len(i.KernelSymbols)))
		}
	} else {
		sb.WriteString("Target: dyld_shared_cache / User binaries\n")
	}
	sb.WriteString(`
Load this script in IDA Pro with the appropriate binary loaded.
It will:
  1. Print the panic backtrace to the output window (addresses are clickable)
  2. Add comments at each frame address (including crash timestamp/ID)
  3. Highlight each frame with a color unique to this crash
  4. Jump to the first (crashing) frame`)
	if isKernel && len(i.KernelSymbols) > 0 {
		sb.WriteString("\n  5. Apply kernel symbols from kernel analysis (rename addresses)")
	}
	sb.WriteString(`
"""
import idc
import idaapi
import ida_kernwin

# Crash metadata (for comments and identification)
CRASH_INFO = {
`)
	sb.WriteString(fmt.Sprintf("    \"timestamp\": \"%s\",\n", escapePyString(timestamp)))
	sb.WriteString(fmt.Sprintf("    \"incident_id\": \"%s\",\n", escapePyString(i.Header.IncidentID)))
	sb.WriteString(fmt.Sprintf("    \"device\": \"%s\",\n", escapePyString(i.Payload.Product)))
	sb.WriteString(fmt.Sprintf("    \"os_version\": \"%s (%s)\",\n", escapePyString(i.Header.Version()), escapePyString(i.Header.Build())))
	sb.WriteString(fmt.Sprintf("    \"crashlog\": \"%s\",\n", escapePyString(filepath.Base(i.Config.CrashlogPath))))
	sb.WriteString(`}

`)
	sb.WriteString(fmt.Sprintf("# Panic frame highlight color (BGR format) - unique per crash\nPANIC_COLOR = 0x%06x\n\n", panicColor))
	sb.WriteString(`# Frame data: (address, symbol, frame_index, image_name)
frames = [
`)

	// Write frame data (skip null address frames - stack terminators)
	for idx, frame := range frames {
		// For kernel frames, ImageOffset is the unslid address
		// For user frames, we need to handle slide
		addr := frame.ImageOffset
		if !isKernel && frame.Slide > 0 {
			// Unslide user frame addresses for static analysis in IDA
			addr = frame.ImageOffset - frame.Slide
		}

		// Skip null address frames (stack terminators/sentinels)
		if addr == 0 {
			continue
		}

		symbol := frame.Symbol
		if symbol == "" {
			symbol = fmt.Sprintf("sub_%x", addr)
		}
		symbol = escapePyString(symbol)

		imageName := frame.ImageName
		if imageName == "" {
			imageName = "unknown"
		}
		imageName = escapePyString(imageName)

		sb.WriteString(fmt.Sprintf("    (0x%x, \"%s\", %d, \"%s\"),\n", addr, symbol, idx, imageName))
	}

	sb.WriteString("]\n\n")

	// Add kernel symbols if available (only for kernel scripts)
	if isKernel && len(i.KernelSymbols) > 0 {
		sb.WriteString(fmt.Sprintf("# Kernel symbols from kernel analysis (%d symbols)\n", len(i.KernelSymbols)))
		sb.WriteString("# Format: {address: \"symbol_name\", ...}\n")
		sb.WriteString("kernel_symbols = {\n")

		// Sort addresses for deterministic output
		addrs := make([]uint64, 0, len(i.KernelSymbols))
		for addr := range i.KernelSymbols {
			addrs = append(addrs, addr)
		}
		slices.Sort(addrs)

		for _, addr := range addrs {
			sym := escapePyString(i.KernelSymbols[addr])
			sb.WriteString(fmt.Sprintf("    0x%x: \"%s\",\n", addr, sym))
		}
		sb.WriteString("}\n")
	} else {
		sb.WriteString("# No kernel symbols available\n")
		sb.WriteString("kernel_symbols = {}\n")
	}

	// Write script body
	sb.WriteString(`
def main():
    # Build short ID for comments (first 8 chars of incident ID)
    short_id = CRASH_INFO["incident_id"][:8] if CRASH_INFO["incident_id"] else "unknown"

    print("=" * 70)
    print(f"Panic Backtrace - {CRASH_INFO['device']} {CRASH_INFO['os_version']}")
    print(f"  Timestamp:   {CRASH_INFO['timestamp']}")
    print(f"  Incident ID: {CRASH_INFO['incident_id']}")
    print(f"  Crashlog:    {CRASH_INFO['crashlog']}")
`)
	if isKernel {
		sb.WriteString("    print(\"  Target:      Kernelcache\")\n")
	} else {
		sb.WriteString("    print(\"  Target:      dyld_shared_cache / User binaries\")\n")
	}
	sb.WriteString(`    print("=" * 70)

    marked = 0
    for addr, symbol, idx, image in frames:
        # Check if address exists in the database
        if not idc.is_mapped(addr):
            print(f"[{idx:2d}] 0x{addr:016x}  {symbol}  ({image}) - NOT MAPPED")
            continue

        # Print to output window (addresses are clickable in IDA)
        print(f"[{idx:2d}] 0x{addr:016x}  {symbol}  ({image})")

        # Set comment at address with crash identification
        # Format: [Crash <short_id> #<frame>] <timestamp> - <symbol>
        comment = f"[Crash {short_id} #{idx}] {CRASH_INFO['timestamp']} - {symbol}"
        existing = idc.get_cmt(addr, 0)
        if existing:
            comment = f"{existing}; {comment}"
        idc.set_cmt(addr, comment, 0)

        # Highlight the instruction line with crash-specific color
        idc.set_color(addr, idc.CIC_ITEM, PANIC_COLOR)

        marked += 1

    print("=" * 70)
    print(f"Marked {marked}/{len(frames)} panic frames (color: 0x{PANIC_COLOR:06x})")
`)

	// Add kernel symbol application code if we have symbols
	if isKernel && len(i.KernelSymbols) > 0 {
		sb.WriteString(`
    # Apply kernel symbols from signature analysis
    print("")
    print("=" * 70)
    print(f"Applying {len(kernel_symbols)} kernel symbols from signature analysis...")
    print("=" * 70)

    symbols_applied = 0
    symbols_skipped = 0
    symbols_unmapped = 0

    for addr, symbol in kernel_symbols.items():
        if not idc.is_mapped(addr):
            symbols_unmapped += 1
            continue

        # Get existing name at address
        existing_name = idc.get_name(addr)

        # Skip if already has a meaningful name (not auto-generated)
        if existing_name and not existing_name.startswith(("sub_", "loc_", "unk_", "byte_", "word_", "dword_", "qword_")):
            symbols_skipped += 1
            continue

        # Set the function name
        # SN_NOCHECK = 0x00, SN_NOLIST = 0x80, SN_NOWARN = 0x100, SN_FORCE = 0x800
        if idc.set_name(addr, symbol, idc.SN_NOCHECK | 0x800):
            symbols_applied += 1
        else:
            # Try without force flag
            if idc.set_name(addr, symbol, idc.SN_NOCHECK):
                symbols_applied += 1
            else:
                symbols_skipped += 1

    print(f"Symbols applied: {symbols_applied}")
    print(f"Symbols skipped (already named): {symbols_skipped}")
    print(f"Symbols unmapped: {symbols_unmapped}")
    print("=" * 70)
`)
	}

	sb.WriteString(`
    # Jump to the first (crashing) frame if it exists
    if frames and idc.is_mapped(frames[0][0]):
        ida_kernwin.jumpto(frames[0][0])
        print(f"Jumped to crash location: 0x{frames[0][0]:x}")

    # Refresh the view
    idaapi.refresh_idaview_anyway()

if __name__ == "__main__":
    main()
`)

	// Write script to file
	if err := os.WriteFile(scriptPath, []byte(sb.String()), 0644); err != nil {
		return fmt.Errorf("failed to write IDA script to %s: %w", scriptPath, err)
	}

	log.WithField("script", scriptPath).Info("Generated IDAPython script")
	return nil
}
