import AuthenticationServices
import Foundation
import Observation
import UIKit

// 扫码登录（新设备侧）+ 会话持有。iOS 显示二维码，已登录的 web / 桌面设备用“扫码登录”
// 扫码批准，本端轮询 /api/auth/qr/status 拿到自签会话。字段 / 流程见后端 internal/httpapi/
// qr.go。登录是纯 REST（不经引擎 WebView），故 engine.html 未部署也能登录。
//
// 续期（Auth Broker 路径 A）：access_token 默认 900s；引擎（WebView）持注入的 refresh_token，
// 到期经 POST /api/auth/refresh {refresh_token} 自刷（cdrop 代理 broker，见 refresh.go），broker
// 轮换 refresh 后经桥 sessionRotated 回报本端更新 Keychain；refresh 过期 / 吊销则经 authExpired
// 回登录页重扫。本端不再依赖 cdrop_session cookie。
@MainActor
@Observable
final class AuthManager
{
    struct User: Codable
    {
        let id: String
        let name: String
        let avatar: String?
    }

    struct Session: Codable
    {
        let accessToken: String
        // refreshToken：broker 委托会话的续期凭证，随 access 一并注入引擎自刷用；轮换后更新。
        let refreshToken: String
        let user: User
        let deviceName: String
        // deviceId：cdrop 生成的稳定不透明设备 id（= broker meta，回显 X-Auth-Meta）。
        let deviceId: String
        let scope: String
    }

    var session: Session?
    var qrPayload: String?
    // 空初值：登录页以 broker 账号登录为主，无操作时不显状态文案；各登录流程开始时自行设置。
    var statusText: String = ""
    // 二维码已失效（denied/expired/失败）→ 登录页据此提示并高亮「刷新二维码」。
    var qrExpired = false

    // 登录代次：每次 startQRLogin / startBrokerLogin 自增，旧流程据此识别自己已被取代、丢弃
    // 结果，避免两条登录流程并发改 statusText / session 打架。
    private var loginGeneration = 0

    // Broker 登录期间强持 ASWebAuthenticationSession 封装（其 presentationContextProvider 为 weak、
    // session 自身亦须保活至回调），完成 / 失败后置空。
    private var brokerFlow: BrokerAuthFlow?

    // Keychain 持久化坐标（单 app 私有，见 Keychain.swift）。
    private static let keychainService = "net.commilitia.cdrop.session"
    private static let keychainAccount = "session"

    // 启动即尝试恢复持久会话：命中则 app 直接进主界面，免反复扫码（迭代 / 重装友好）。
    // 恢复的令牌可能已过期（guest 1h 上限）——此时引擎鉴权 401，用户经设置页登出重扫即可。
    init()
    {
        if let data = Keychain.load(service: Self.keychainService, account: Self.keychainAccount),
           let restored = try? JSONDecoder().decode(Session.self, from: data)
        {
            // 仅恢复完整权限会话；陈旧 guest（旧版本落地的）丢弃、强制重登为 full。
            if restored.scope == "full" { session = restored }
            else { Keychain.delete(service: Self.keychainService, account: Self.keychainAccount) }
        }
    }

    // 后端基址：默认线上，可经 CDROP_API_BASE 覆盖（本机测试）。
    private var apiBase: String
    {
        ProcessInfo.processInfo.environment["CDROP_API_BASE"] ?? "https://drop.commilitia.net"
    }

    private struct QRStart: Decodable
    {
        let request_id: String
        let poll_secret: String
        let qr_payload: String
        let expires_at: Int64
    }

    private struct QRStatus: Decodable
    {
        let status: String
        let access_token: String?
        let refresh_token: String?
        let device_id: String?
        let expires_in: Int?
        let user: User?
        let device_name: String?
    }

    // startQRLogin：发起二维码 + 轮询直到批准 / 失效。可重复调用（刷新二维码）：自增代次，
    // 旧轮询识别到代次变更即弃。
    func startQRLogin() async
    {
        loginGeneration += 1
        let gen = loginGeneration
        qrExpired = false
        qrPayload = nil
        statusText = t("ios.login.generating")
        do
        {
            let start = try await qrStart()
            if gen != loginGeneration { return }
            qrPayload = start.qr_payload
            statusText = t("ios.login.waiting")
            try await poll(requestID: start.request_id, pollSecret: start.poll_secret, gen: gen)
        }
        catch
        {
            if gen != loginGeneration { return }
            statusText = t("ios.login.failed")
            qrExpired = true
        }
    }

    // 应用内 Broker 登录（device-authorization + PKCE，见 BrokerLogin.swift）：手机直接输账号登录，
    // 无需另一台设备扫码。流程＝拉 broker 配置 → ASWebAuthenticationSession 授权拿 code → 换 bootstrap
    // 令牌 → 代铸成 cdrop 设备会话（带真名 / 稳定 device_id）→ 落 full 会话 + Keychain。
    func startBrokerLogin() async
    {
        loginGeneration += 1
        let gen = loginGeneration
        qrExpired = false
        statusText = t("ios.login.brokerStarting")
        do
        {
            let cfg = try await fetchAuthConfig()
            guard !cfg.brokerURL.isEmpty, !cfg.brokerApp.isEmpty else { throw BrokerLoginError.incompleteConfig }

            let verifier = PKCE.randomToken(64)
            let challenge = PKCE.challenge(for: verifier)
            let state = PKCE.randomToken(24)
            let redirectURI = "cdrop://auth-callback"

            var comp = URLComponents(string: cfg.brokerURL.trimmingTrailingSlash() + "/device/authorize")!
            comp.queryItems = [
                URLQueryItem(name: "app", value: cfg.brokerApp),
                URLQueryItem(name: "redirect_uri", value: redirectURI),
                URLQueryItem(name: "state", value: state),
                URLQueryItem(name: "code_challenge", value: challenge),
                URLQueryItem(name: "code_challenge_method", value: "S256"),
                URLQueryItem(name: "description", value: "Commilitia Drop iOS"),
            ]
            guard let authURL = comp.url else { throw BrokerLoginError.incompleteConfig }

            let flow = BrokerAuthFlow()
            brokerFlow = flow
            let callback = try await flow.run(url: authURL, callbackScheme: "cdrop")
            brokerFlow = nil
            if gen != loginGeneration { return }

            let items = URLComponents(url: callback, resolvingAgainstBaseURL: false)?.queryItems ?? []
            guard items.first(where: { $0.name == "state" })?.value == state
            else { throw BrokerLoginError.stateMismatch }
            guard let code = items.first(where: { $0.name == "code" })?.value, !code.isEmpty
            else { throw BrokerLoginError.missingCode }

            let bootstrap = try await exchangeDeviceToken(
                brokerURL: cfg.brokerURL, code: code, verifier: verifier, redirectURI: redirectURI)
            let ds = try await mintDeviceSession(bootstrap: bootstrap)
            if gen != loginGeneration { return }

            DeviceIDStore.value = ds.device_id
            let realName = (ds.name?.isEmpty == false) ? ds.name! : ds.user_id
            session = Session(accessToken: ds.access_token,
                              refreshToken: ds.refresh_token,
                              user: User(id: ds.user_id, name: realName, avatar: ds.avatar),
                              deviceName: ds.device_name ?? DeviceNameStore.value,
                              deviceId: ds.device_id,
                              scope: "full")
            persist()
        }
        catch
        {
            brokerFlow = nil
            if gen != loginGeneration { return }
            // 用户主动取消（关闭授权页）不算失败：静默回到登录页等待再次操作。
            if let asErr = error as? ASWebAuthenticationSessionError, asErr.code == .canceledLogin
            {
                statusText = t("ios.login.waiting")
                return
            }
            statusText = t("ios.login.failed")
        }
    }

    func logout()
    {
        session = nil
        qrPayload = nil
        statusText = ""
        Keychain.delete(service: Self.keychainService, account: Self.keychainAccount)
    }

    // ---- Broker 登录辅助：配置拉取 / 令牌交换 / 代铸 ----

    private struct AuthConfig { let brokerURL: String; let brokerApp: String }

    private struct DeviceTokenResp: Decodable
    {
        let access: String
        let refresh: String
        let access_expires: Int64
    }

    // 代铸响应：access/refresh + 稳定 device_id + 经边缘核验的真实身份（user_id/name/avatar）。
    private struct DeviceSessionResp: Decodable
    {
        let access_token: String
        let refresh_token: String
        let device_id: String
        let device_name: String?
        let user_id: String
        let name: String?
        let avatar: String?
    }

    private func fetchAuthConfig() async throws -> AuthConfig
    {
        let (data, _) = try await URLSession.shared.data(from: URL(string: "\(apiBase)/api/auth/config")!)
        let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
        return AuthConfig(brokerURL: obj["broker_url"] as? String ?? "",
                          brokerApp: obj["broker_app"] as? String ?? "")
    }

    private func exchangeDeviceToken(
        brokerURL: String, code: String, verifier: String, redirectURI: String) async throws -> String
    {
        var req = URLRequest(url: URL(string: brokerURL.trimmingTrailingSlash() + "/device/token")!)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("application/json", forHTTPHeaderField: "Accept")
        req.httpBody = try JSONSerialization.data(withJSONObject: [
            "code": code, "code_verifier": verifier, "redirect_uri": redirectURI,
        ])
        let (data, resp) = try await URLSession.shared.data(for: req)
        let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
        guard status == 200 else { throw BrokerLoginError.tokenFailed(status) }
        let tr = try JSONDecoder().decode(DeviceTokenResp.self, from: data)
        guard !tr.access.isEmpty else { throw BrokerLoginError.badResponse }
        return tr.access
    }

    private func mintDeviceSession(bootstrap: String) async throws -> DeviceSessionResp
    {
        var req = URLRequest(url: URL(string: "\(apiBase)/api/auth/device-session")!)
        req.httpMethod = "POST"
        req.setValue("Bearer \(bootstrap)", forHTTPHeaderField: "Authorization")
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("ios", forHTTPHeaderField: "X-Device-Type")
        req.httpBody = try JSONSerialization.data(withJSONObject: [
            "device_id": DeviceIDStore.value,
            "device_name": DeviceNameStore.value,
            "device_type": "ios",
        ])
        let (data, resp) = try await URLSession.shared.data(for: req)
        let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
        guard status == 200 else { throw BrokerLoginError.deviceSessionFailed(status) }
        return try JSONDecoder().decode(DeviceSessionResp.self, from: data)
    }

    // 持久化当前会话到 Keychain（扫码批准后调用）。
    private func persist()
    {
        guard let session, let data = try? JSONEncoder().encode(session) else { return }
        Keychain.save(data, service: Self.keychainService, account: Self.keychainAccount)
    }

    // updateSession：引擎自刷致 broker 轮换后，经桥 sessionRotated 回报的新令牌对 → 更新
    // 内存会话 + Keychain，使重启后不再用已失效的旧 refresh。仅换令牌，身份 / 设备名不变。
    func updateSession(accessToken: String, refreshToken: String)
    {
        guard let cur = session else { return }
        session = Session(accessToken: accessToken, refreshToken: refreshToken,
                          user: cur.user, deviceName: cur.deviceName,
                          deviceId: cur.deviceId, scope: cur.scope)
        persist()
    }

    // updateIdentity：引擎据 /api/me 取到真实显示名（QR 登录注入的是 subject UUID 名）→ 经桥
    // identityUpdated 回报，更新会话身份 + Keychain，使设置页显示真名而非 UUID（#12）。仅换
    // 显示身份，令牌 / 设备名不变。
    func updateIdentity(name: String, avatar: String?)
    {
        guard let cur = session else { return }
        let av = (avatar?.isEmpty == false) ? avatar : cur.user.avatar
        session = Session(accessToken: cur.accessToken, refreshToken: cur.refreshToken,
                          user: User(id: cur.user.id, name: name, avatar: av),
                          deviceName: cur.deviceName, deviceId: cur.deviceId, scope: cur.scope)
        persist()
    }

    // updateDeviceName：本机即时改名成功（PATCH /api/devices/{device_id}）后经桥 renamed 回报，
    // 更新会话里的设备名 + Keychain，使重启后 boot 注入新名。令牌 / 身份不变——设备名与令牌解耦。
    func updateDeviceName(_ name: String)
    {
        guard let cur = session else { return }
        session = Session(accessToken: cur.accessToken, refreshToken: cur.refreshToken,
                          user: cur.user, deviceName: name,
                          deviceId: cur.deviceId, scope: cur.scope)
        persist()
    }

    // 仅供本机模拟器联调（CDROP_DEBUG_SESSION=1）：注入占位会话跳过扫码，用于验证引擎从
    // prod 加载 + 桥往返。令牌无效、鉴权会 401，但能证 engine.html 在 WKWebView 里加载运行。
    func debugSession()
    {
        session = Session(accessToken: "debug", refreshToken: "",
                          user: User(id: "debug", name: "Simulator", avatar: nil),
                          deviceName: "Simulator", deviceId: "", scope: "guest")
    }

    private func qrStart() async throws -> QRStart
    {
        var req = URLRequest(url: URL(string: "\(apiBase)/api/auth/qr/start")!)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("ios", forHTTPHeaderField: "X-Device-Type")
        req.httpBody = try JSONSerialization.data(
            withJSONObject: [ "device_name": deviceName(), "device_type": "ios" ])
        let (data, _) = try await URLSession.shared.data(for: req)
        return try JSONDecoder().decode(QRStart.self, from: data)
    }

    private func poll(requestID: String, pollSecret: String, gen: Int) async throws
    {
        while session == nil && gen == loginGeneration
        {
            var comp = URLComponents(string: "\(apiBase)/api/auth/qr/status")!
            comp.queryItems = [ URLQueryItem(name: "request_id", value: requestID) ]
            var req = URLRequest(url: comp.url!)
            req.setValue(pollSecret, forHTTPHeaderField: "X-Poll-Secret")
            let (data, _) = try await URLSession.shared.data(for: req)
            if gen != loginGeneration { return }   // 已被刷新取代，丢弃本轮结果
            let st = try JSONDecoder().decode(QRStatus.self, from: data)
            switch st.status
            {
            case "approved":
                if let token = st.access_token, let user = st.user
                {
                    // 原生 App 仅允许完整权限（用户原则：iOS/桌面只接受 full，受限访客是 Web/PWA
                    // 的权宜）。批准方若按「仅此次」授予 guest，这里据 /api/me 的真实 scope 拒绝、
                    // 提示改选「信任此设备」，并让二维码可刷新重扫——不落地 guest 会话。
                    let scope = await fetchScope(token: token)
                    if gen != loginGeneration { return }
                    if scope != "full"
                    {
                        statusText = t("ios.login.needFull")
                        qrExpired = true
                        return
                    }
                    session = Session(accessToken: token,
                                      refreshToken: st.refresh_token ?? "",
                                      user: user,
                                      deviceName: st.device_name ?? deviceName(),
                                      deviceId: st.device_id ?? "",
                                      scope: "full")
                    persist()
                }
                return
            case "denied", "expired":
                statusText = t("ios.login.expired")
                qrExpired = true
                return
            default:
                continue   // pending → 立即下一轮（服务端长轮询最多挂 25s）
            }
        }
    }

    // 据刚发的 access token 拉 /api/me 取真实会话级别（full / guest）。失败回空串（按非 full
    // 处理，保守拒绝）。
    private func fetchScope(token: String) async -> String
    {
        var req = URLRequest(url: URL(string: "\(apiBase)/api/me")!)
        req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        guard let (data, _) = try? await URLSession.shared.data(for: req),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let scope = obj["scope"] as? String
        else { return "" }
        return scope
    }

    private func deviceName() -> String
    {
        return DeviceNameStore.value
    }
}
