#if HWTEST
#if !SELFTEST
#error HWTEST は SELFTEST の部品（SelfTestReport）を使うため，SELFTEST と同時に定義すること
#endif
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace TIASshot {

    /// <summary>
    /// 実機テストのタイムアウト時に止まった場所を記録する（HWTEST 限定．本番・SELFTEST のみのビルドには含まれない）．
    /// ウォッチドッグが項目・全体の上限を超えたと判定したときだけ呼ばれ，
    /// UI スレッド・保存スレッドのマネージドスタックを結果ファイルの詳細行に，自プロセスのミニダンプを作業フォルダに書く．
    /// どちらも上限時間を設けた別スレッドで行い，取得に失敗・時間切れしてもウォッチドッグの流れ（光源のポートを閉じる → 終了コード 3）は止めない．
    /// </summary>
    internal static partial class HwTestRunner {

        /// <summary>
        /// UI スレッドと保存スレッド（生きていれば）のマネージドスタックを取り，結果ファイルの詳細行（"  - " の後ろ）にする行の並びを返す．
        /// 失敗・時間切れの場合も理由を行にして返す（例外は漏らさない）．
        /// </summary>
        private static List<string> CaptureHangStacks(Session session) {
            var lines = new List<string>();
            AppendThreadStack(lines, "UI スレッド", session.UiThread);
            Thread saveThread = null;
            try {
                saveThread = session.Camera?.HwTestSaveThread;
            } catch (Exception ex) {
                lines.Add($"保存スレッド: 参照を取れません: {ex.GetType().Name}: {ex.Message}");
                Debug.WriteLine($"[HWTEST] 保存スレッドの参照を取れません: {ex}");
            }
            if (saveThread == null) {
                lines.Add("保存スレッド: なし（まだ起動していない）");
            } else if (!saveThread.IsAlive) {
                lines.Add("保存スレッド: 終了済み");
            } else {
                AppendThreadStack(lines, "保存スレッド", saveThread);
            }
            return lines;
        }

        /// <summary>
        /// 1 スレッドのマネージドスタックを取る．取得は別スレッドで行い，HwTestThresholds.HangStackTimeout で打ち切る
        /// （Suspend が安全点に届かない等でウォッチドッグ自体が止まらないため）．
        /// </summary>
        private static void AppendThreadStack(List<string> lines, string label, Thread target) {
            if (target == null) {
                lines.Add($"{label}: スレッドが記録されていません");
                return;
            }
            var name = $"{label}（ManagedThreadId {target.ManagedThreadId}{(string.IsNullOrEmpty(target.Name) ? "" : $"，{target.Name}")}）";
            if (!target.IsAlive) {
                lines.Add($"{name}: 終了済みのためスタックを取れません");
                return;
            }
            string stack = null;
            string error = null;
            var helper = new Thread(() => {
                try {
                    stack = CaptureStack(target);
                } catch (Exception ex) {
                    error = $"{ex.GetType().Name}: {ex.Message}";
                    Debug.WriteLine($"[HWTEST] {label}のスタックを取れません: {ex}");
                }
            }) { IsBackground = true, Name = "HwTest.StackCapture" };
            helper.Start();
            if (!helper.Join(HwTestThresholds.HangStackTimeout)) {
                lines.Add($"{name}: スタックの取得が {HwTestThresholds.HangStackTimeout.TotalSeconds:0} 秒以内に終わりません（Suspend が戻らない可能性．ダンプで確認してください）");
                return;
            }
            if (error != null) {
                lines.Add($"{name}: スタックを取れません: {error}");
                return;
            }
            var frames = stack.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
            lines.Add($"{name}のマネージドスタック（{frames.Length} 行．上が最も内側．ネイティブのフレームは含まない）:");
            foreach (var f in frames) lines.Add($"    {f.Trim()}");
        }

        /// <summary>
        /// 対象スレッドを一時停止してマネージドスタックを取り，必ず再開する．
        /// .NET Framework 4.8 で他スレッドのスタックを取る唯一の手段のため，Obsolete の Thread.Suspend / Resume と
        /// StackTrace(Thread, bool) を使う（HWTEST 限定．タイムアウト後にプロセスを終了する直前だけ呼ぶ）．
        /// </summary>
        private static string CaptureStack(Thread target) {
#pragma warning disable 618   // Thread.Suspend / Resume・StackTrace(Thread, bool) は Obsolete（他スレッドのスタックを取る目的に限って使う）
            target.Suspend();
            try {
                return new StackTrace(target, true).ToString();
            } finally {
                target.Resume();
            }
#pragma warning restore 618
        }

        /// <summary>
        /// 自プロセスのミニダンプを作業フォルダ（HwTestJudge.HangDumpPath）に書き，パスと大きさ（書けなければ WARN）を結果ファイルに出す．
        /// 種類（全メモリ／小さいダンプ／書かない）はプライベートメモリと空き容量から HwTestJudge.ChooseDumpKind で決める．
        /// 書き出しは別スレッドで行い，HwTestThresholds.HangDumpTimeout で打ち切る（例外は漏らさない）．
        /// </summary>
        private static void WriteHangDump(Session session) {
            var report = session.Report;
            string path;
            HwTestDumpKind kind;
            long privateBytes = -1;
            long freeBytes = -1;
            try {
                path = HwTestJudge.HangDumpPath(session.WorkDir, DateTime.Now);
                Directory.CreateDirectory(session.WorkDir);
                using (var self = Process.GetCurrentProcess()) privateBytes = self.PrivateMemorySize64;
                var root = Path.GetPathRoot(Path.GetFullPath(path));
                freeBytes = new DriveInfo(root).AvailableFreeSpace;
                kind = HwTestJudge.ChooseDumpKind(privateBytes, freeBytes);
            } catch (Exception ex) {
                report.WriteLine($"  WARN: ミニダンプの準備に失敗したため書きません: {SelfTestReport.DescribeException(ex)}");
                return;
            }
            if (kind == HwTestDumpKind.None) {
                report.WriteLine($"  WARN: 空き容量が足りないためミニダンプを書きません（空き {FormatBytes(freeBytes)}，必要 {FormatBytes(HwTestThresholds.SmallDumpMinFreeBytes)} 以上）");
                return;
            }

            string error = null;
            var sw = Stopwatch.StartNew();
            var helper = new Thread(() => {
                try {
                    WriteMiniDump(path, HwTestJudge.DumpTypeFlags(kind));
                } catch (Exception ex) {
                    error = SelfTestReport.DescribeException(ex);
                    Debug.WriteLine($"[HWTEST] ミニダンプを書けません: {path}: {ex}");
                }
            }) { IsBackground = true, Name = "HwTest.MiniDump" };
            helper.Start();
            var kindText = kind == HwTestDumpKind.Full
                ? "全メモリ"
                : $"小さいダンプ（全メモリにしなかった理由: プライベートメモリ {FormatBytes(privateBytes)}，空き {FormatBytes(freeBytes)}）";
            if (!helper.Join(HwTestThresholds.HangDumpTimeout)) {
                report.WriteLine($"  WARN: ミニダンプの書き出しが {HwTestThresholds.HangDumpTimeout.TotalSeconds:0} 秒以内に終わりません（{kindText}．途中までのファイルが残る可能性: {path}）");
                return;
            }
            if (error != null) {
                report.WriteLine($"  WARN: ミニダンプを書けませんでした（{kindText}）: {path}: {error}");
                return;
            }
            long size = -1;
            try {
                size = new FileInfo(path).Length;
            } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) {
                Debug.WriteLine($"[HWTEST] ミニダンプの大きさを取れません: {path}: {ex.Message}");
            }
            report.WriteLine($"  - ミニダンプ: {path}（{kindText}，{(size >= 0 ? FormatBytes(size) : "大きさ不明")}，{sw.ElapsedMilliseconds} ms．"
                + $"プライベートメモリ {FormatBytes(privateBytes)}．MINIDUMP_TYPE 0x{HwTestJudge.DumpTypeFlags(kind):X}）");
        }

        /// <summary>dbghelp の MiniDumpWriteDump で自プロセスのダンプを書く（失敗は Win32Exception．途中まで書いたファイルは残す）</summary>
        private static void WriteMiniDump(string path, uint dumpType) {
            using (var self = Process.GetCurrentProcess())
            using (var stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) {
                if (!NativeMethods.MiniDumpWriteDump(self.Handle, (uint)self.Id, stream.SafeFileHandle, dumpType,
                        IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) {
                    // MiniDumpWriteDump は失敗の理由を HRESULT として GetLastError で返す
                    var err = Marshal.GetLastWin32Error();
                    throw new Win32Exception(err, $"MiniDumpWriteDump が失敗しました（0x{err:X8}）");
                }
            }
        }

        private static string FormatBytes(long bytes) {
            if (bytes < 0) return "不明";
            return $"{bytes / (1024.0 * 1024.0):0.0} MB";
        }

        private static class NativeMethods {
            [DllImport("dbghelp.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeFileHandle hFile, uint dumpType,
                IntPtr exceptionParam, IntPtr userStreamParam, IntPtr callbackParam);
        }
    }
}
#endif
