/**
 * JKES 校园跑：GPS / calc / pause / end（对齐 jkes_test/simulateRun.js）
 * - 默认按「目标距离 + 配速」由闭合轨迹几何展开时间与速度，不依赖 path_data 中存的 distance/点内配速
 * - 结束前必须先 pause，否则服务端记为无效
 */
const Redis = require('../DataBase/Redis.js')
const jkesRedisKeys = require('./redisKeys.js')

const { getJkesSettings, normalizeApiBase } = require('./jkesSettings')
const { postJkes } = require('./jkesHttp')
const {
    isJkesLoginExpiredPayload,
    makeJkesLoginExpiredError
} = require('./request')

const CALC_INTERVAL_MS = 50000

const R_EARTH = 6371000

function toRad(d) {
    return (d * Math.PI) / 180
}

function haversineM(lat1, lon1, lat2, lon2) {
    const la1 = toRad(lat1)
    const la2 = toRad(lat2)
    const dLat = toRad(lat2 - lat1)
    const dLon = toRad(lon2 - lon1)
    const h =
        Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2
    return 2 * R_EARTH * Math.asin(Math.min(1, Math.sqrt(h)))
}

function interpolateLngLat(a, b, t) {
    return {
        latitude: a.latitude + (b.latitude - a.latitude) * t,
        longitude: a.longitude + (b.longitude - a.longitude) * t
    }
}

/** 首末点距离超过约 3m 时视为未闭合，补上一段回到起点 */
function ensureClosedLoop(loop) {
    if (loop.length < 2) {
        throw new Error('闭合轨迹至少 2 个点')
    }
    const first = loop[0]
    const last = loop[loop.length - 1]
    const gap = haversineM(first.latitude, first.longitude, last.latitude, last.longitude)
    if (gap > 3) {
        return loop.concat([{ latitude: first.latitude, longitude: first.longitude }])
    }
    return loop
}

/** 库内 path_data：支持 latitude/longitude 或 a/o */
function pathRawToLoop(raw) {
    if (!Array.isArray(raw) || raw.length === 0) {
        throw new Error('轨迹数据应为非空数组')
    }
    const loop = raw.map((p, i) => {
        if (typeof (p.latitude ?? p.lat) === 'number') {
            return {
                latitude: p.latitude ?? p.lat,
                longitude: p.longitude ?? p.lon ?? p.o
            }
        }
        if (typeof p.a === 'number' && typeof p.o === 'number') {
            return { latitude: p.a, longitude: p.o }
        }
        throw new Error(`第 ${i + 1} 个点无有效坐标（需 latitude/longitude 或 a/o）`)
    })
    return ensureClosedLoop(loop)
}

/**
 * 沿闭合环路走够 targetM 米；点序列含起点，末点为终点（可能落在边上插值）
 * （与 simulateRun.js 一致）
 */
function expandLoopToDistance(loop, targetM) {
    const n = loop.length
    if (n < 2) throw new Error('闭合轨迹至少 2 个点')
    const out = [{ latitude: loop[0].latitude, longitude: loop[0].longitude }]
    let cum = 0
    let vi = 0
    let guard = 0
    const maxGuard = Math.ceil(targetM * 3) + n * 200

    while (cum < targetM - 1e-6 && guard++ < maxGuard) {
        const next = (vi + 1) % n
        const a = loop[vi]
        const b = loop[next]
        const edge = haversineM(a.latitude, a.longitude, b.latitude, b.longitude)
        if (edge < 1e-9) {
            vi = next
            continue
        }
        const remain = targetM - cum
        if (edge <= remain + 1e-9) {
            cum += edge
            vi = next
            out.push({ latitude: b.latitude, longitude: b.longitude })
            if (cum >= targetM - 1e-9) break
        } else {
            const t = remain / edge
            const p = interpolateLngLat(a, b, t)
            out.push({ latitude: p.latitude, longitude: p.longitude })
            cum = targetM
            break
        }
    }
    if (guard >= maxGuard) {
        throw new Error('展开轨迹失败：边长过短或无法沿环前进，请检查轨迹是否闭合')
    }
    return out
}

/** 按配速生成 deviceTimeRaw(ms) 与 speed(m/s) */
function scheduleByPace(points, paceSecPerKm) {
    const secPerM = paceSecPerKm / 1000
    const rows = []
    let tMs = 0
    for (let i = 0; i < points.length; i++) {
        const p = points[i]
        if (i === 0) {
            rows.push({
                latitude: p.latitude,
                longitude: p.longitude,
                deviceTimeRaw: 0,
                speed: -1,
                steps: 0
            })
            continue
        }
        const prev = points[i - 1]
        const dM = haversineM(prev.latitude, prev.longitude, p.latitude, p.longitude)
        const dtMs = Math.max(0, dM * secPerM * 1000)
        tMs += dtMs
        const v = dtMs > 0 ? dM / (dtMs / 1000) : -1
        rows.push({
            latitude: p.latitude,
            longitude: p.longitude,
            deviceTimeRaw: Math.round(tMs),
            speed: v >= 0 && v < 0.05 ? -1 : Math.round(v * 100) / 100,
            steps: 0
        })
    }
    return rows
}

function buildPointsFromDistanceAndPace(raw, distanceM, paceSecPerKm) {
    const dm = Number(distanceM)
    const pace = Number(paceSecPerKm)
    if (!Number.isFinite(dm) || dm < 1) {
        throw new Error('distanceM 无效')
    }
    if (!Number.isFinite(pace) || pace < 10) {
        throw new Error('paceSecPerKm 无效（每公里秒数，建议 ≥120）')
    }
    const loop = pathRawToLoop(raw)
    const expanded = expandLoopToDistance(loop, dm)
    return scheduleByPace(expanded, pace)
}

/** 旧版 path.json：点内自带 d 时间轴与 s 速度 */
function normalizePathPointsLegacy(raw) {
    if (!Array.isArray(raw) || raw.length === 0) {
        throw new Error('轨迹数据应为非空数组')
    }
    return raw.map((p, i) => {
        const d = String(p.d || '')
        const ts = parseInt(d.split(/\s+/)[0], 10)
        if (!Number.isFinite(ts)) {
            throw new Error(`第 ${i + 1} 个点缺少有效 d 字段时间戳（legacy 模式）`)
        }
        return {
            latitude: p.a,
            longitude: p.o,
            deviceTimeRaw: ts,
            speed: typeof p.s === 'number' ? p.s : -1,
            steps: typeof p.b === 'number' ? p.b : 0
        }
    })
}

function remapDeviceTimes(points, runStartMs) {
    const t0 = points[0].deviceTimeRaw
    return points.map((p) => ({
        ...p,
        deviceTime: runStartMs + (p.deviceTimeRaw - t0)
    }))
}

function toGpsPayloadPoint(p, opts) {
    const acc = opts.defaultAccuracy
    const spd = p.speed >= 0 ? p.speed : -1
    return {
        verticalAccuracy: 30,
        speed: spd,
        longitude: p.longitude,
        horizontalAccuracy: acc,
        provider: 'gps',
        steps: p.steps,
        latitude: p.latitude,
        accuracy: acc,
        direction: -1,
        altitude: opts.altitude,
        type: 'gcj02',
        deviceTime: p.deviceTime
    }
}

function chunkPoints(points, firstN, restN) {
    if (points.length === 0) return []
    const chunks = []
    const first = points.slice(0, firstN)
    if (first.length) chunks.push(first)
    let i = first.length
    while (i < points.length) {
        chunks.push(points.slice(i, i + restN))
        i += restN
    }
    return chunks
}

function sleep(ms) {
    return new Promise((r) => setTimeout(r, ms))
}

/** 根据轨迹首尾 deviceTime 得到预计跑完全程的等待时长（与循环内 sleep 总和一致，不含网络请求） */
function estimateRunWallClockMs(points) {
    if (!Array.isArray(points) || points.length < 2) {
        return 0
    }
    return Math.max(0, points[points.length - 1].deviceTime - points[0].deviceTime)
}

function formatMinutesSeconds(ms) {
    const totalSec = Math.max(0, Math.round(ms / 1000))
    const m = Math.floor(totalSec / 60)
    const s = totalSec % 60
    if (m <= 0) {
        return `${s}秒`
    }
    return `${m}分${s}秒`
}

/**
 * @param {object} opts
 * @param {string} opts.token
 * @param {Array} opts.pathPoints path_data.data：经纬度环（a/o 或 latitude/longitude）
 * @param {number} [opts.distanceM] 目标跑步距离（米）；与 paceSecPerKm 同时传入则走新版调度
 * @param {number} [opts.paceSecPerKm] 每公里用时（秒），如 390 ≈ 6:30/km
 * @param {string} [opts.baseUrl]
 * @param {number} [opts.batchSize=5]
 * @param {number} [opts.firstBatchSize=1]
 * @param {number} [opts.altitude]
 * @param {number} [opts.defaultAccuracy]
 * @param {function} [opts.log]
 * @param {'auto'|'direct'|'proxy'} [opts.outboundMode='auto']
 * @param {string} [opts.traceId]
 * @param {string} [opts.taskId]
 */
async function runJkesRecord(opts) {
    const s = getJkesSettings()
    const {
        token,
        recordDbId,
        pathPoints: rawPoints,
        distanceM,
        paceSecPerKm,
        baseUrl = normalizeApiBase(s.apiBase),
        batchSize = 5,
        firstBatchSize = 1,
        altitude = s.gpsAltitude,
        defaultAccuracy = s.gpsDefaultAccuracy,
        log = () => { },
        outboundMode = 'auto',
        traceId = null,
        taskId = null
    } = opts

    if (!token || String(token).trim() === '') {
        throw new Error('缺少 JKES token')
    }

    const useScheduled =
        distanceM != null &&
        paceSecPerKm != null &&
        Number.isFinite(Number(distanceM)) &&
        Number.isFinite(Number(paceSecPerKm))

    const pointsRaw = useScheduled
        ? buildPointsFromDistanceAndPace(rawPoints, distanceM, paceSecPerKm)
        : normalizePathPointsLegacy(rawPoints)

    const runStartMs = Date.now()
    const points = remapDeviceTimes(pointsRaw, runStartMs)

    if (useScheduled) {
        log(
            `配速模式 目标 ${(Number(distanceM) / 1000).toFixed(2)}km pace=${paceSecPerKm}s/km 点数=${points.length}`
        )
    }

    const headers = {
        'content-type': 'application/json',
        'x-auth-token': String(token).trim(),
        'user-agent': s.userAgent,
        referer: s.referer,
        'Accept-Encoding': 'gzip,compress,br,deflate'
    }

    const requestTimeout = Math.max(120000, Number(s.requestTimeoutMs) || 0)
    const httpLogger = { info: (msg) => log(String(msg).replace(/^\[jkesHttp\]\s*/, '')) }

    const postJson = async (pathSuffix, body) => {
        const url = `${baseUrl.replace(/\/$/, '')}${pathSuffix.startsWith('/') ? '' : '/'}${pathSuffix}`
        const res = await postJkes(url, body, {
            headers,
            timeout: requestTimeout,
            validateStatus: () => true,
            outboundMode,
            traceId,
            taskId,
            logger: httpLogger
        })
        const text = typeof res.data === 'object' ? JSON.stringify(res.data) : String(res.data)
        let json = res.data
        if (typeof json !== 'object' || json === null) {
            try {
                json = JSON.parse(text)
            } catch {
                throw new Error(`JKES 非 JSON 响应 ${res.status}: ${String(text).slice(0, 200)}`)
            }
        }
        if (isJkesLoginExpiredPayload(json)) {
            throw makeJkesLoginExpiredError(json)
        }
        if (res.status !== 200 || json.code !== 0) {
            const err = new Error(`JKES 请求失败 ${res.status} ${pathSuffix}: ${String(text).slice(0, 500)}`)
            err.retryable = res.status >= 500 || res.status === 0
            throw err
        }
        return json
    }

    const startJson = await postJson('/health/runRecord/startRecord/0', {
        deviceTime: runStartMs,
        latitude: points[0].latitude,
        longitude: points[0].longitude,
        accuracy: defaultAccuracy,
        speed: points[0].speed >= 0 ? points[0].speed : -1
    })
    const recordId = startJson.data.info.id
    const chunks = chunkPoints(points, firstBatchSize, batchSize)
    const estMs = estimateRunWallClockMs(points)
    log(`已开始跑步 recordId=${recordId} 预计耗时${formatMinutesSeconds(estMs)}`)

    const calcState = {
        runStartMs,
        nextCalcDueDeviceTime: runStartMs + CALC_INTERVAL_MS
    }

    async function flushCalcThroughDeviceTime(throughDeviceTime) {
        while (calcState.nextCalcDueDeviceTime <= throughDeviceTime) {
            await postJson(`/health/runRecord/calc/${recordId}`, {})
            calcState.nextCalcDueDeviceTime += CALC_INTERVAL_MS
        }
    }
    const uploadedPayloadPoints = []
    let prevSegmentEndDeviceTime = null

    for (let c = 0; c < chunks.length; c++) {
        const chunk = chunks[c]
        if (c > 0) {
            const gap = chunk[0].deviceTime - prevSegmentEndDeviceTime
            const waitMs = Math.max(0, gap)
            if (waitMs > 0) {
                await sleep(waitMs)
            }
        }

        const tStart = chunk[0].deviceTime
        const tEnd = chunk[chunk.length - 1].deviceTime

        await flushCalcThroughDeviceTime(tStart)

        const batch = chunk.map((p) => toGpsPayloadPoint(p, { altitude, defaultAccuracy }))
        await postJson(`/health/runRecord/gps/${recordId}`, batch)
        uploadedPayloadPoints.push(...batch)

        await flushCalcThroughDeviceTime(tEnd)

        await Redis.set(jkesRedisKeys.lepaoSchedule(recordDbId), JSON.stringify({current: c, total: chunks.length}), { EX: 60 * 60 * 3 })

        const intraMs = Math.max(0, tEnd - tStart)
        if (intraMs > 0) {
            await sleep(intraMs)
        }

        prevSegmentEndDeviceTime = tEnd
    }

    await postJson(`/health/runRecord/pause/${recordId}`, {})
    const endJson = await postJson(`/health/runRecord/end/${recordId}`, {})
    log(`ID:${recordId} 跑步已结束`)
    await Redis.del(jkesRedisKeys.lepaoSchedule(recordDbId))
    return { recordId, endJson, runStartMs, uploadedPayloadPoints }
}

module.exports = {
    runJkesRecord,
    /** @deprecated 仅兼容旧脚本；Worker 已改用 distanceM + paceSecPerKm */
    normalizePathPoints: normalizePathPointsLegacy,
    buildPointsFromDistanceAndPace,
    get DEFAULT_BASE() {
        return normalizeApiBase(getJkesSettings().apiBase)
    }
}
