#import "TQNavigationDelegate.h"
#import "TQDownloadManager.h"
#import "TQDownloadResponseHandler.h"
#import "TQSdkEvent.h"
#import "TQLogger.h"
#import <CoreTelephony/CTCellularData.h>
#import <UIKit/UIKit.h>

#pragma mark - 常量

/// 重试次数耗尽后的最终监听周期性重试间隔和次数
static const NSTimeInterval kFinalRetryInterval = 5.0;
static const NSInteger kFinalRetryMaxCount = 6;

/// 网络权限弹窗导致首次加载失败时的自动重试上限。
static const NSInteger kMaxNetworkRetry = 5;

/// 渲染进程连续崩溃恢复次数上限，超过则判定不可恢复并停止 reload（对齐 Android）。
static const NSInteger kMaxRenderRecovery = 3;

/// 崩溃恢复计数的冷却窗口（秒）。距上次崩溃超过此窗口视为页面已稳定运行过，
/// 本次崩溃按全新问题处理并重置连续计数，避免长时间运行中的偶发（非连续）崩溃
/// 累计触顶被误判为"不可恢复"。
static const NSTimeInterval kRenderRecoveryResetWindow = 30;

/// 渲染进程崩溃后延迟恢复的秒数。崩溃(尤其 OOM)后立即 reload 易因内存仍紧张再次被回收
/// 陷入循环，延迟给系统留资源回收窗口(对齐社区实践 / HarmonyOS RECOVER_DELAY_MS)。
static const NSTimeInterval kRenderRecoveryDelay = 0.5;

/// NSURLErrorDataNotAllowed:蜂窝数据未授权
static const NSInteger kNSURLErrorDataNotAllowed = -1020;

@interface TQNavigationDelegate ()

@property (nonatomic, strong) TQWebViewConfig *config;
@property (nonatomic, strong) TQSchemeInterceptor *schemeInterceptor;
@property (nonatomic, strong) TQExternalSchemeHandler *externalSchemeHandler;
@property (nonatomic, copy, nullable) TQOnEventBlock onEvent;
@property (nonatomic, weak, nullable) TQDownloadManager *downloadManager;

/// 受控 WKWebView weak ref。仅用于 lifetime 通知(didBecomeActive)
/// 触发白屏自检时取回 webView,delegate 不持有 webView 强引用。
@property (nonatomic, weak, nullable) WKWebView *boundWebView;
/// 常驻 didBecomeActive 监听:覆盖 App 切到后台后渲染进程被回收的场景。
/// 与网络重试用的 `becomeActiveObserver` 区分,生命周期跟随 delegate。
@property (nonatomic, strong, nullable) id lifetimeBecomeActiveObserver;

/// 网络权限弹窗导致首次加载失败时的自动重试计数器。
@property (nonatomic, assign) NSInteger networkRetryCount;

/// 渲染进程崩溃连续恢复计数，防止"必崩页面"死循环 reload。
/// 每次崩溃恢复 +1，页面成功加载完成（didFinishNavigation）时清零；连续崩溃达
/// kMaxRenderRecovery 次仍未成功 → 判定不可恢复，停止 reload。对齐 Android renderRecoveryCount。
@property (nonatomic, assign) NSInteger renderRecoveryCount;

/// 上次渲染进程崩溃的时间戳（主线程访问，无并发）。用于冷却窗口重置。
@property (nonatomic, assign) NSTimeInterval lastRenderTerminateAt;

/// CTCellularData:监听 App 网络权限状态变化,用户授权后自动 reload
@property (nonatomic, strong, nullable) CTCellularData *cellularData;
@property (nonatomic, strong, nullable) dispatch_block_t networkRetryFallbackWorkItem;
@property (nonatomic, assign) BOOL hasNetworkRetryReloaded;
/// CTCellularData 初始化后会立即触发一次当前状态回调,需要跳过
@property (nonatomic, assign) BOOL isFirstCellularCallback;
/// didBecomeActive 通知观察者
@property (nonatomic, strong, nullable) id becomeActiveObserver;
/// 保存最近一次 provisional navigation 的 URL,用于网络权限恢复后重新加载。
/// 因为 provisional navigation 失败后 webView.URL 为 nil,reload 无效,
/// 必须用 loadRequest 重新发起请求。
@property (nonatomic, strong, nullable) NSURL *lastProvisionalURL;

/// final 阶段的周期性重试 timer
@property (nonatomic, strong, nullable) dispatch_source_t finalRetryTimer;
@property (nonatomic, assign) NSInteger finalRetryRemaining;

@end

@implementation TQNavigationDelegate

- (instancetype)initWithConfig:(TQWebViewConfig *)config
              schemeInterceptor:(TQSchemeInterceptor *)schemeInterceptor
          externalSchemeHandler:(TQExternalSchemeHandler *)externalSchemeHandler
                        onEvent:(TQOnEventBlock)onEvent {
    if (self = [super init]) {
        _config = [config copy];
        _schemeInterceptor = schemeInterceptor;
        _externalSchemeHandler = externalSchemeHandler;
        _onEvent = [onEvent copy];
        [self installLifetimeBecomeActiveObserver];
    }
    return self;
}

- (void)dealloc {
    // 用户关闭页面时,确保 CTCellularData 监听和兜底定时器被清理,防止泄漏
    [self cancelNetworkMonitor];
    if (_lifetimeBecomeActiveObserver) {
        [[NSNotificationCenter defaultCenter] removeObserver:_lifetimeBecomeActiveObserver];
        _lifetimeBecomeActiveObserver = nil;
    }
}

- (void)setDownloadManager:(TQDownloadManager *)manager {
    _downloadManager = manager;
}

- (void)bindWebView:(WKWebView *)webView {
    self.boundWebView = webView;
}

#pragma mark - 白屏自检

/// 监听 didBecomeActive，覆盖 App 切后台导致渲染进程回收后回前台白屏的场景。
- (void)installLifetimeBecomeActiveObserver {
    __weak __typeof(self) weakSelf = self;
    self.lifetimeBecomeActiveObserver = [[NSNotificationCenter defaultCenter]
        addObserverForName:UIApplicationDidBecomeActiveNotification
                    object:nil
                     queue:[NSOperationQueue mainQueue]
                usingBlock:^(NSNotification *_) {
        [weakSelf attemptBlankRecoveryIfNeeded];
    }];
}

- (void)attemptBlankRecoveryIfNeeded {
    WKWebView *webView = self.boundWebView;
    if (webView == nil) {
        return;
    }
    if (webView.isLoading) {
        return;
    }
    NSString *currentUrlString = webView.URL.absoluteString;
    BOOL isBlankUrl = (currentUrlString.length == 0)
                   || [currentUrlString isEqualToString:@"about:blank"];
    if (!isBlankUrl) {
        return;
    }
    if (self.lastProvisionalURL == nil) {
        return;
    }
    TQLogNavigationInfo("[BlankRecovery] webView appears blank (url=%{public}@, loading=%d), reloading lastProvisionalURL=%{public}@",
                        currentUrlString ?: @"nil",
                        webView.isLoading,
                        self.lastProvisionalURL.absoluteString);
    [self reloadWebView:webView];
}

#pragma mark - 拦截

- (void)webView:(WKWebView *)webView
    decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
    preferences:(WKWebpagePreferences *)preferences
    decisionHandler:(void (^)(WKNavigationActionPolicy, WKWebpagePreferences *))decisionHandler API_AVAILABLE(ios(13.0)) {
    [self handleNavigationAction:navigationAction completion:^(WKNavigationActionPolicy policy) {
        decisionHandler(policy, preferences);
    }];
}

/// iOS 11-12 导航决策入口（无 preferences 参数）
- (void)webView:(WKWebView *)webView
    decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
    decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    [self handleNavigationAction:navigationAction completion:decisionHandler];
}

/// 公共导航决策逻辑（scheme 拦截 → http(s) 下载检测 → external scheme → 兜底取消）
- (void)handleNavigationAction:(WKNavigationAction *)navigationAction
                    completion:(void (^)(WKNavigationActionPolicy))completion {
    NSURL *url = navigationAction.request.URL;
    if (url == nil) {
        completion(WKNavigationActionPolicyCancel);
        return;
    }
    NSString *urlString = url.absoluteString ?: @"";

    // 1. qianapp
    if ([self.schemeInterceptor handle:urlString]) {
        completion(WKNavigationActionPolicyCancel);
        return;
    }

    // 2. http(s) 走默认(但先检查 shouldPerformDownload)
    NSString *scheme = [url.scheme lowercaseString] ?: @"";
    if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
        // iOS 14.5+: <a download> 属性触发 → 直接走 URLSession 下载
        if (@available(iOS 14.5, *)) {
            if (navigationAction.shouldPerformDownload) {
                completion(WKNavigationActionPolicyCancel);
                [self.downloadManager downloadURL:url
                                          response:nil
                                         dataStore:nil
                               downloadWithCookies:self.config.downloadWithCookies];
                return;
            }
        }
        // 保存主框架导航的 URL,用于网络权限恢复后重新加载
        BOOL isMainFrame = navigationAction.targetFrame ? navigationAction.targetFrame.isMainFrame : YES;
        if (isMainFrame) {
            self.lastProvisionalURL = url;
        }
        completion(WKNavigationActionPolicyAllow);
        return;
    }

    // 3 & 4. external + 兜底
    if ([self.externalSchemeHandler tryOpen:urlString]) {
        completion(WKNavigationActionPolicyCancel);
        return;
    }

    completion(WKNavigationActionPolicyCancel);
}

- (void)webView:(WKWebView *)webView
    decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
    decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    // 下载处理
    TQDownloadDecision decision = [TQDownloadResponseHandler decideForResponse:navigationResponse
                                                                        onEvent:self.onEvent];
    switch (decision) {
        case TQDownloadDecisionCancel:
            decisionHandler(WKNavigationResponsePolicyCancel);
            return;
        case TQDownloadDecisionDownload: {
            // 统一走 URLSession 下载(全版本),按配置决定是否同步 Cookie
            decisionHandler(WKNavigationResponsePolicyCancel);
            NSURL *url = navigationResponse.response.URL;
            if (url) {
                [self.downloadManager downloadURL:url
                                          response:navigationResponse.response
                                         dataStore:nil
                               downloadWithCookies:self.config.downloadWithCookies];
            }
            return;
        }
        case TQDownloadDecisionAllow:
            break;
    }

    // 主框架 HTTP 错误
    if (navigationResponse.isForMainFrame &&
        [navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *http = (NSHTTPURLResponse *)navigationResponse.response;
        if (http.statusCode >= 400 && self.onEvent) {
            self.onEvent([[TQHttpErrorEvent alloc]
                          initWithUrl:http.URL.absoluteString ?: @""
                           statusCode:http.statusCode
                         reasonPhrase:[NSHTTPURLResponse localizedStringForStatusCode:http.statusCode]]);
        }
    }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

#pragma mark - 生命周期

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    if (self.onEvent) {
        NSString *url = webView.URL.absoluteString
                     ?: self.lastProvisionalURL.absoluteString
                     ?: @"";
        self.onEvent([[TQPageStartedEvent alloc] initWithUrl:url]);
    }
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    // 页面加载成功,重置网络重试计数器并清理 monitor
    self.networkRetryCount = 0;
    // 页面成功加载完成 → 崩溃恢复计数清零，但**必须门控稳定运行窗口**：
    // OOM 型必崩页面每次都是"加载完成 → 运行几秒 → 再崩"，若无条件清零，则 didFinish
    // 会把刚累加的计数抹掉，renderRecoveryCount 永远停在 1，kMaxRenderRecovery 上限形同
    // 虚设、陷入无限 reload。仅当距上次崩溃超过 kRenderRecoveryResetWindow（真正稳定运行过）
    // 才清零；否则保留计数，让连续崩溃能累加到上限并停止 reload。
    // 从未崩溃过的正常页面：lastRenderTerminateAt 初始 0，差值远超窗口，照常清零（本就是 0，no-op）。
    if ([[NSDate date] timeIntervalSince1970] - self.lastRenderTerminateAt > kRenderRecoveryResetWindow) {
        self.renderRecoveryCount = 0;
    }
    [self cancelNetworkMonitor];
    if (self.onEvent) {
        self.onEvent([[TQPageFinishedEvent alloc]
                      initWithUrl:webView.URL.absoluteString ?: @""]);
    }
}

#pragma mark - 错误

- (void)webView:(WKWebView *)webView
    didFailProvisionalNavigation:(WKNavigation *)navigation
    withError:(NSError *)error {
    NSString *url = webView.URL.absoluteString ?: @"";
    NSInteger code = error.code;

    // iOS 首次安装弹出"允许使用无线数据"权限弹窗时,
    // WKWebView 的请求会被系统阻断,触发网络错误。
    // 用户确认授权后系统不会自动重试,需要 SDK 监听网络恢复后自动 reload。
    if ([error.domain isEqualToString:NSURLErrorDomain] &&
        [TQNavigationDelegate isNetworkPermissionError:code]) {
        if (self.networkRetryCount < kMaxNetworkRetry) {
            self.networkRetryCount += 1;
            TQLogNavigationInfo("[NetworkRetry] Error detected: domain=%{public}@, code=%ld, attempt=%ld/%ld",
                                error.domain, (long)code,
                                (long)self.networkRetryCount, (long)kMaxNetworkRetry);
            if (self.onEvent) {
                NSString *desc = [NSString stringWithFormat:@"[SDK auto-retry %ld/%ld] %@",
                                  (long)self.networkRetryCount, (long)kMaxNetworkRetry,
                                  error.localizedDescription];
                self.onEvent([[TQLoadErrorEvent alloc] initWithUrl:url
                                                          errorCode:code
                                                        description:desc]);
            }
            [self startNetworkMonitorForReloadWithWebView:webView];
            return;
        }
        // 重试次数耗尽,上报错误,但保留 CTCellularData 监听。
        // 这样即使用户等待很久才点"允许",权限恢复后仍能触发最后一次 reload。
        TQLogNavigationInfo("[NetworkRetry] Max retries exhausted (%ld), keeping CTCellularData monitor for late permission grant",
                            (long)kMaxNetworkRetry);
        [self reportError:error url:url];
        // 仅在 finalRetryTimer 未运行时才启动最终监听
        if (self.finalRetryTimer == nil) {
            [self startFinalNetworkMonitorWithWebView:webView];
        }
        return;
    }
    [self reportError:error url:url];
}

- (void)webView:(WKWebView *)webView
    didFailNavigation:(WKNavigation *)navigation
    withError:(NSError *)error {
    [self reportError:error url:webView.URL.absoluteString ?: @""];
}

- (void)reportError:(NSError *)error url:(NSString *)url {
    NSInteger code = error.code;
    // SSL 类错误特殊上报
    if ([error.domain isEqualToString:NSURLErrorDomain] &&
        (code == NSURLErrorServerCertificateUntrusted ||
         code == NSURLErrorServerCertificateHasBadDate ||
         code == NSURLErrorServerCertificateHasUnknownRoot ||
         code == NSURLErrorServerCertificateNotYetValid ||
         code == NSURLErrorClientCertificateRejected ||
         code == NSURLErrorClientCertificateRequired)) {
        if (self.onEvent) {
            self.onEvent([[TQSslErrorEvent alloc] initWithUrl:url primaryError:code]);
        }
        return;
    }
    if (self.onEvent) {
        self.onEvent([[TQLoadErrorEvent alloc] initWithUrl:url
                                                  errorCode:code
                                                description:error.localizedDescription ?: @""]);
    }
}

/// 判断是否为网络权限相关的错误码。
/// iOS 首次安装 App 时弹出"允许使用无线数据"弹窗,
/// 在用户确认前 WKWebView 请求会失败,常见错误码包括:
/// - NSURLErrorNotConnectedToInternet (-1009)
/// - NSURLErrorNetworkConnectionLost (-1005)
/// - NSURLErrorDataNotAllowed (-1020):蜂窝数据未授权
+ (BOOL)isNetworkPermissionError:(NSInteger)code {
    return code == NSURLErrorNotConnectedToInternet
        || code == NSURLErrorNetworkConnectionLost
        || code == kNSURLErrorDataNotAllowed;
}

#pragma mark - Network Permission Monitor

/// 启动网络权限监听(初始重试阶段)。
/// 三重检测:CTCellularData + didBecomeActive + 3 秒兜底超时。
/// 任一先触发即 reload 并清理所有监听。
- (void)startNetworkMonitorForReloadWithWebView:(WKWebView *)webView {
    [self startNetworkMonitorWithWebView:webView phase:@"retry"];

    // 兜底:3 秒后如果 CTCellularData 和 didBecomeActive 均未触发,也尝试 reload 一次。
    __weak __typeof(self) weakSelf = self;
    __weak WKWebView *weakWebView = webView;
    dispatch_block_t fallbackItem = dispatch_block_create(0, ^{
        [weakSelf tryReloadOnceWithWebView:weakWebView trigger:@"fallback timeout (3s)"];
    });
    self.networkRetryFallbackWorkItem = fallbackItem;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), fallbackItem);
}

/// 启动网络权限监听(重试耗尽后的最终阶段)。
/// 三重检测:CTCellularData + didBecomeActive + 周期性重试(每 5 秒,最多 6 次 = 30 秒)。
- (void)startFinalNetworkMonitorWithWebView:(WKWebView *)webView {
    [self startNetworkMonitorWithWebView:webView phase:@"final"];

    // 周期性重试兜底:覆盖纯 Wi-Fi 设备 CTCellularData 不回调的场景
    self.finalRetryRemaining = kFinalRetryMaxCount;
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
                                                     dispatch_get_main_queue());
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kFinalRetryInterval * NSEC_PER_SEC)),
                              (uint64_t)(kFinalRetryInterval * NSEC_PER_SEC),
                              0);
    __weak __typeof(self) weakSelf = self;
    __weak WKWebView *weakWebView = webView;
    dispatch_source_set_event_handler(timer, ^{
        __strong __typeof(weakSelf) self = weakSelf;
        if (!self || self.hasNetworkRetryReloaded) {
            [self cancelNetworkMonitor];
            return;
        }
        WKWebView *wv = weakWebView;
        if (!wv) {
            TQLogNavigationInfo("[NetworkRetry] WebView already released, cancelling final periodic retry");
            [self cancelNetworkMonitor];
            return;
        }
        self.finalRetryRemaining -= 1;
        TQLogNavigationInfo("[NetworkRetry] Final periodic retry, remaining=%ld",
                            (long)self.finalRetryRemaining);
        [self reloadWebView:wv];
        if (self.finalRetryRemaining <= 0) {
            TQLogNavigationInfo("[NetworkRetry] Final periodic retries exhausted, giving up completely");
            [self cancelNetworkMonitor];
        }
    });
    self.finalRetryTimer = timer;
    dispatch_resume(timer);
}

/// 启动 CTCellularData + didBecomeActive 双重监听。
- (void)startNetworkMonitorWithWebView:(WKWebView *)webView phase:(NSString *)phase {
    // 先清理可能存在的旧监听(防御性编程)
    [self cancelNetworkMonitor];
    self.hasNetworkRetryReloaded = NO;
    self.isFirstCellularCallback = YES;

    CTCellularData *cellular = [[CTCellularData alloc] init];
    self.cellularData = cellular;

    TQLogNavigationInfo("[NetworkRetry] [%{public}@] Monitor started: CTCellularData + didBecomeActive", phase);

    // CTCellularData 监听
    __weak __typeof(self) weakSelf = self;
    __weak WKWebView *weakWebView = webView;
    NSString *capturedPhase = [phase copy];
    cellular.cellularDataRestrictionDidUpdateNotifier = ^(CTCellularDataRestrictedState state) {
        __strong __typeof(weakSelf) self = weakSelf;
        if (!self) return;

        NSString *stateDesc;
        switch (state) {
            case kCTCellularDataRestrictedStateUnknown: stateDesc = @"unknown"; break;
            case kCTCellularDataRestricted:             stateDesc = @"restricted"; break;
            case kCTCellularDataNotRestricted:          stateDesc = @"notRestricted"; break;
            default: stateDesc = [NSString stringWithFormat:@"unrecognized(%ld)", (long)state]; break;
        }
        TQLogNavigationInfo("[NetworkRetry] [%{public}@] CTCellularData callback: state=%{public}@, isFirst=%d, hasReloaded=%d",
                            capturedPhase, stateDesc,
                            self.isFirstCellularCallback, self.hasNetworkRetryReloaded);

        // CTCellularData 初始化后会立即触发一次当前状态回调,需要跳过
        if (self.isFirstCellularCallback) {
            self.isFirstCellularCallback = NO;
            TQLogNavigationInfo("[NetworkRetry] [%{public}@] Skipping initial CTCellularData callback", capturedPhase);
            return;
        }

        // 用户点击"允许"后,状态变为 notRestricted
        // CTCellularData 回调在后台线程,统一切到主线程操作避免数据竞争
        if (state == kCTCellularDataNotRestricted) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSString *trigger = [NSString stringWithFormat:@"[%@] CTCellularData → notRestricted", capturedPhase];
                [weakSelf tryReloadOnceWithWebView:weakWebView trigger:trigger];
            });
        }
    };

    // 监听 App 从 inactive → active(系统弹窗消失时触发)。
    // 网络权限弹窗会导致 App 进入 inactive 状态,
    // 用户点击"允许"或"不允许"后 App 恢复 active,此时立即尝试 reload。
    // 作为 CTCellularData 的补充:CTCellularData 在某些设备/系统版本上可能不回调,
    // 但 didBecomeActive 是系统级通知,可靠性更高。
    self.becomeActiveObserver = [[NSNotificationCenter defaultCenter]
        addObserverForName:UIApplicationDidBecomeActiveNotification
                    object:nil
                     queue:[NSOperationQueue mainQueue]
                usingBlock:^(NSNotification *note) {
        NSString *trigger = [NSString stringWithFormat:@"[%@] App became active", capturedPhase];
        [weakSelf tryReloadOnceWithWebView:weakWebView trigger:trigger];
    }];
}

/// 尝试执行一次 reload(first-wins 竞争模式)。
/// 多个检测源(CTCellularData、didBecomeActive、兜底超时)共享此方法,
/// 通过 `hasNetworkRetryReloaded` 标志位保证只有第一个触发的源执行 reload。
- (void)tryReloadOnceWithWebView:(WKWebView *)webView trigger:(NSString *)trigger {
    if (self.hasNetworkRetryReloaded) return;
    self.hasNetworkRetryReloaded = YES;
    TQLogNavigationInfo("[NetworkRetry] Triggered by: %{public}@", trigger);
    if (webView == nil) {
        TQLogNavigationInfo("[NetworkRetry] WebView already released, skip reload");
        [self cancelNetworkMonitor];
        return;
    }
    [self reloadWebView:webView];
    [self cancelNetworkMonitor];
}

/// 重新加载 WebView。
/// 优先使用保存的 lastProvisionalURL 发起新请求,
/// 因为 provisional navigation 失败后 webView.URL 为 nil,reload 无效。
- (void)reloadWebView:(WKWebView *)webView {
    if (self.lastProvisionalURL) {
        TQLogNavigationInfo("[NetworkRetry] Re-loading URL: %{public}@", self.lastProvisionalURL.absoluteString);
        [webView loadRequest:[NSURLRequest requestWithURL:self.lastProvisionalURL]];
    } else if (webView.URL) {
        TQLogNavigationInfo("[NetworkRetry] Fallback reload with webView.URL: %{public}@", webView.URL.absoluteString);
        [webView reload];
    } else {
        TQLogNavigationInfo("[NetworkRetry] No URL available for reload, skipping");
    }
}

/// 取消网络权限监听和兜底定时器,释放资源。
/// 在以下场景调用:
/// 1. 权限授予成功 reload 后
/// 2. 兜底超时触发后
/// 3. 页面加载成功后(didFinish)
/// 4. dealloc(用户关闭页面)
- (void)cancelNetworkMonitor {
    if (self.cellularData) {
        self.cellularData.cellularDataRestrictionDidUpdateNotifier = nil;
        self.cellularData = nil;
    }
    if (self.networkRetryFallbackWorkItem) {
        dispatch_block_cancel(self.networkRetryFallbackWorkItem);
        self.networkRetryFallbackWorkItem = nil;
    }
    if (self.finalRetryTimer) {
        dispatch_source_cancel(self.finalRetryTimer);
        self.finalRetryTimer = nil;
    }
    if (self.becomeActiveObserver) {
        [[NSNotificationCenter defaultCenter] removeObserver:self.becomeActiveObserver];
        self.becomeActiveObserver = nil;
    }
}

#pragma mark - SSL challenge

- (void)webView:(WKWebView *)webView
    didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
    completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
    // 默认信任处理:走系统默认链路。**永不**自定义信任非法证书。
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

#pragma mark - Web Content 进程终止恢复

/// WKWebView 的 Web Content 进程被系统杀死时回调(iOS 低内存时触发)。
///
/// **典型触发场景**:
/// - 老机型(如 iPhone 8 Plus)运行内存吃紧
/// - H5 页面调起 `<input type="file">` 拍照,系统相机 + UIImagePickerController
///   占用大量内存,WKWebView 渲染进程被优先回收
/// - 后台被系统因内存压力杀死渲染进程
///
/// **重要陷阱**:进程被杀后 `webView.URL` 通常会变为 nil,
/// 此时 `[webView reload]` 不会发起任何请求,导致永远白屏。
/// 因此必须使用保存的 `lastProvisionalURL`,通过 `loadRequest:` 重新发起请求。
/// 复用 `reloadWebView:` 方法保持与网络权限恢复路径一致的容错策略。
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
    NSString *currentUrl = webView.URL.absoluteString
                         ?: self.lastProvisionalURL.absoluteString
                         ?: @"";
    if (self.onEvent) {
        self.onEvent([[TQWebContentProcessTerminatedEvent alloc] initWithUrl:currentUrl]);
    }
    // 冷却重置：距上次崩溃超过 kRenderRecoveryResetWindow 视为页面已稳定运行过，
    // 本次按全新问题处理，重置连续计数，避免偶发崩溃被误判为"不可恢复"。
    NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
    if (now - self.lastRenderTerminateAt > kRenderRecoveryResetWindow) {
        self.renderRecoveryCount = 0;
    }
    self.lastRenderTerminateAt = now;
    // 连续崩溃达上限 → 判定不可恢复,停止 reload,避免死循环 reload 风暴。
    // 客户可据已上报的 TQWebContentProcessTerminatedEvent 自行决定关闭页面。
    self.renderRecoveryCount += 1;
    if (self.renderRecoveryCount > kMaxRenderRecovery) {
        TQLogNavigationInfo("[ProcessTerminated] crashed %ld times, giving up recovery",
                            (long)self.renderRecoveryCount);
        return;
    }
    TQLogNavigationInfo("[ProcessTerminated] Web Content process killed, will recover after %.1fs for url=%{public}@",
                        kRenderRecoveryDelay, currentUrl);
    // 延迟恢复,避免崩溃后立即 reload 因内存仍紧张再次被回收(循环崩溃)。
    // 注意:进程死后 webView.URL 多数情况下为 nil,不能直接调用 reload,
    // 必须优先用 lastProvisionalURL 通过 loadRequest 发起新请求。
    // 用 __weak 捕获:延迟窗口内用户关闭页面时 self/webView 已释放,回调安全跳过(防野指针)。
    __weak __typeof(self) weakSelf = self;
    __weak WKWebView *weakWebView = webView;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kRenderRecoveryDelay * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        WKWebView *strongWebView = weakWebView;
        if (strongSelf == nil || strongWebView == nil) {
            return;
        }
        [strongSelf reloadWebView:strongWebView];
    });
}

@end
