package wang.yaojia.webterm.nav

import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.window.core.layout.WindowSizeClass
import wang.yaojia.webterm.hostregistry.Host
import wang.yaojia.webterm.wiring.AppEnvironment
import wang.yaojia.webterm.wiring.ColdStartRoute

/**
 * # WebTerm navigation host (app-assembly of A19–A29).
 *
 * The single `NavHost` for the app: it maps each [NavRoutes] destination onto the real screen pane
 * ([PairingPane] / [SessionsHome] / [ProjectsHome] / [TerminalHost] / [ProjectDetailPane] / [DiffPane] /
 * [ClientCertPane] / [GatePane]), wires the inter-screen callbacks to `navController` navigations, and
 * resolves every collaborator (ViewModels, per-host `ApiClient`, the terminal holder) off [env]. The
 * start destination is chosen by [wang.yaojia.webterm.wiring.ColdStartPolicy] (surfaced through
 * [NavRoutes.startRouteFor]) and passed in as [startRoute].
 *
 * ## Deep links go through [DeepLinkRouter], NOT `navDeepLink`
 * A `navDeepLink { uriPattern = "webterminal://open?host={host}&join={join}" }` would let the OS navigate
 * on ANY `{host}`/`{join}` value, bypassing the v4-UUID whitelist. Instead the launcher Activity (and the
 * push tap) run [DeepLinkRouter.route] first and navigate only a whitelist-validated [NavRoutes.forDeepLink]
 * route — the manifest `<intent-filter>`s deliver the intent; validation is never delegated to the graph.
 * That is the single non-skippable check (plan §8 "App Links").
 *
 * The `NavHost` composition itself is device-QA (plan §7); the pure route mappers ([NavRoutes]) are
 * JVM-unit-tested (`DeepLinkRouterTest`, `NavRoutesTest`).
 */
@Composable
public fun WebTermNavHost(
    env: AppEnvironment,
    startRoute: String,
    navController: NavHostController = rememberNavController(),
    modifier: Modifier = Modifier,
    onHostPaired: (Host) -> Unit = {},
) {
    NavHost(
        navController = navController,
        startDestination = startRoute,
        modifier = modifier,
    ) {
        composable(NavRoutes.PAIRING) {
            PairingPane(
                env = env,
                onPaired = { host ->
                    onHostPaired(host)
                    navController.navigate(NavRoutes.SESSIONS) {
                        popUpTo(NavRoutes.PAIRING) { inclusive = true }
                        launchSingleTop = true
                    }
                },
                onImportCert = { navController.navigate(NavRoutes.CERT) },
            )
        }

        composable(NavRoutes.SESSIONS) { SessionsHome(env = env, navController = navController) }

        composable(NavRoutes.PROJECTS) { ProjectsHome(env = env, navController = navController) }

        composable(NavRoutes.CERT) { ClientCertPane(env = env, onBack = { navController.popBackStack() }) }

        composable(NavRoutes.ENROLL) { EnrollmentPane(env = env, onBack = { navController.popBackStack() }) }

        composable(
            route = TERMINAL_ROUTE,
            arguments = listOf(
                navArgument(NavArg.HOST_ID) { type = NavType.StringType },
                navArgument(NavArg.SESSION_ID) { type = NavType.StringType },
                navArgument(NavArg.CWD) {
                    type = NavType.StringType
                    nullable = true
                    defaultValue = null
                },
            ),
        ) { entry ->
            val hostId = entry.arguments?.getString(NavArg.HOST_ID)
            TerminalHost(
                env = env,
                hostId = hostId,
                sessionArg = entry.arguments?.getString(NavArg.SESSION_ID),
                cwd = entry.arguments?.getString(NavArg.CWD),
                onNewSessionInCwd = { req ->
                    if (hostId != null) navController.navigate(newTerminalRoute(hostId, req.cwd))
                },
            )
        }

        composable(
            route = PROJECT_DETAIL_ROUTE,
            arguments = listOf(
                navArgument(NavArg.HOST_ID) { type = NavType.StringType },
                navArgument(NavArg.PATH) {
                    type = NavType.StringType
                    nullable = true
                    defaultValue = null
                },
            ),
        ) { entry ->
            ProjectDetailPane(
                env = env,
                hostId = entry.arguments?.getString(NavArg.HOST_ID),
                path = entry.arguments?.getString(NavArg.PATH),
                navController = navController,
            )
        }

        composable(
            route = DIFF_ROUTE,
            arguments = listOf(
                navArgument(NavArg.HOST_ID) { type = NavType.StringType },
                navArgument(NavArg.PATH) {
                    type = NavType.StringType
                    nullable = true
                    defaultValue = null
                },
            ),
        ) { entry ->
            DiffPane(
                env = env,
                hostId = entry.arguments?.getString(NavArg.HOST_ID),
                path = entry.arguments?.getString(NavArg.PATH),
                onBack = { navController.popBackStack() },
            )
        }

        composable(
            route = NavRoutes.GATE_PATTERN,
            arguments = listOf(navArgument(NavArg.SESSION_ID) { type = NavType.StringType }),
        ) { entry ->
            GatePane(
                env = env,
                sessionArg = entry.arguments?.getString(NavArg.SESSION_ID),
                navController = navController,
            )
        }
    }
}

// ── Registered routes carrying an OPTIONAL/encoded arg (kept OUT of the pure [NavRoutes] object) ──────

/** The registered terminal route: [NavRoutes.TERMINAL_PATTERN] plus an optional `cwd` query arg. */
internal val TERMINAL_ROUTE: String = "${NavRoutes.TERMINAL_PATTERN}?${NavArg.CWD}={${NavArg.CWD}}"

/** The registered project-detail route: hostId path arg + an encoded `path` query arg. */
internal val PROJECT_DETAIL_ROUTE: String = "projectDetail/{${NavArg.HOST_ID}}?${NavArg.PATH}={${NavArg.PATH}}"

/** The registered diff route: hostId path arg + an encoded `path` query arg. */
internal val DIFF_ROUTE: String = "diff/{${NavArg.HOST_ID}}?${NavArg.PATH}={${NavArg.PATH}}"

/**
 * Build a concrete route that spawns a NEW session on [hostId] in [cwd] (`attach(null, cwd)`). The cwd
 * is percent-encoded with [Uri.encode] here (the Android layer) so [NavRoutes] stays a pure,
 * JVM-testable object with no `android.net.Uri` edge.
 */
internal fun newTerminalRoute(hostId: String, cwd: String?): String {
    val base = NavRoutes.terminalNew(hostId)
    return if (cwd.isNullOrEmpty()) base else "$base?${NavArg.CWD}=${Uri.encode(cwd)}"
}

/** Build a concrete project-detail route for [hostId] + [path] (path percent-encoded). */
internal fun projectDetailRoute(hostId: String, path: String): String =
    "projectDetail/$hostId?${NavArg.PATH}=${Uri.encode(path)}"

/** Build a concrete diff route for [hostId] + [path] (path percent-encoded). */
internal fun diffRoute(hostId: String, path: String): String =
    "diff/$hostId?${NavArg.PATH}=${Uri.encode(path)}"

/**
 * Extract the layout [WindowWidth] from a Compose [WindowSizeClass] (the thin bridge kept out of the pure
 * [LayoutPolicy]). Mirrors the Material width breakpoints: EXPANDED (≥840dp) → medium (≥600dp) → compact.
 */
internal fun windowWidthOf(sizeClass: WindowSizeClass): WindowWidth = when {
    sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) -> WindowWidth.EXPANDED
    sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) -> WindowWidth.MEDIUM
    else -> WindowWidth.COMPACT
}

/** Nav-arg keys, shared between the route patterns and the argument readers. */
public object NavArg {
    public const val HOST_ID: String = "hostId"
    public const val SESSION_ID: String = "sessionId"

    /** Optional new-session working directory (query arg on the terminal route). */
    public const val CWD: String = "cwd"

    /** Project/diff path (encoded query arg). */
    public const val PATH: String = "path"
}

/**
 * The app's navigation destinations and the pure route mappers. Route strings are the ONE source of
 * truth for both [WebTermNavHost] and the callers that navigate; the mappers are pure (no Android edge)
 * so the cold-start and deep-link seams stay JVM-unit-testable without a device.
 */
public object NavRoutes {
    /** Pairing / add-host flow (A19) — the cold-start landing when no host is paired. */
    public const val PAIRING: String = "pairing"

    /** Session chooser / dashboard (A20) — the cold-start landing when a host is paired. */
    public const val SESSIONS: String = "sessions"

    /** Projects grid (A23). */
    public const val PROJECTS: String = "projects"

    /** Device-certificate management (A27). */
    public const val CERT: String = "cert"

    /** Zero-`.p12` device enrollment (B4) — obtains a hardware-bound cert with no file. */
    public const val ENROLL: String = "enroll"

    /** Terminal route pattern with the required host + session path args (A21). */
    public const val TERMINAL_PATTERN: String = "terminal/{${NavArg.HOST_ID}}/{${NavArg.SESSION_ID}}"

    /**
     * Push-gate route pattern (A30): a session id with NO host id in the payload — the notification
     * handler resolves the host, then this destination surfaces the gate for [sessionId].
     */
    public const val GATE_PATTERN: String = "gate/{${NavArg.SESSION_ID}}"

    /**
     * Sentinel session-arg meaning "spawn a NEW session" (`attach(null)`) — a fresh session has no
     * server id yet, so the terminal route carries this in the [NavArg.SESSION_ID] slot and
     * [attachSessionId] maps it back to `null` at the bind site.
     */
    public const val NEW_SESSION_SENTINEL: String = "new"

    /**
     * The cold-start start destination, keyed off [ColdStartRoute] (no host → pairing, else sessions).
     * Pure; mirrors the [wang.yaojia.webterm.wiring.ColdStartPolicy] decision onto a nav route.
     */
    public fun startRouteFor(route: ColdStartRoute): String = when (route) {
        ColdStartRoute.PAIRING -> PAIRING
        ColdStartRoute.SESSIONS -> SESSIONS
    }

    /** Build a concrete terminal route for a resolved (hostId, sessionId) pair. */
    public fun terminal(hostId: String, sessionId: String): String = "terminal/$hostId/$sessionId"

    /** Build a concrete terminal route that spawns a NEW session on [hostId] ([NEW_SESSION_SENTINEL]). */
    public fun terminalNew(hostId: String): String = "terminal/$hostId/$NEW_SESSION_SENTINEL"

    /**
     * Map a terminal-route session arg to the id to bind: the [NEW_SESSION_SENTINEL] (a fresh spawn)
     * becomes `null` (`attach(null)`), any other value passes through verbatim. Pure → JVM-tested.
     */
    public fun attachSessionId(sessionArg: String?): String? =
        sessionArg?.takeUnless { it == NEW_SESSION_SENTINEL }

    /** Build a concrete push-gate route for a resolved sessionId. */
    public fun gate(sessionId: String): String = "gate/$sessionId"

    /**
     * The deep-link entry point: turn a [DeepLinkRouter] result into the concrete nav route to navigate
     * to, or null for [DeepLinkRoute.Ignore] (the caller does nothing — the ignore is already tallied).
     * This is the ONE place a validated deep link becomes a navigation, so the launcher Activity and the
     * push tap share it. Pure → JVM-tested.
     */
    public fun forDeepLink(route: DeepLinkRoute): String? = when (route) {
        is DeepLinkRoute.OpenSession -> terminal(route.hostId, route.sessionId)
        is DeepLinkRoute.GateSession -> gate(route.sessionId)
        DeepLinkRoute.Ignore -> null
    }
}
