package wang.yaojia.webterm.nav

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import wang.yaojia.webterm.designsystem.Spacing
import wang.yaojia.webterm.hostregistry.Host
import wang.yaojia.webterm.screens.ClientCertScreen
import wang.yaojia.webterm.screens.DiffScreen
import wang.yaojia.webterm.screens.EnrollmentScreen
import wang.yaojia.webterm.screens.PairingScreen
import wang.yaojia.webterm.screens.ProjectDetailScreen
import wang.yaojia.webterm.viewmodels.ApiClientGitWriteGateway
import wang.yaojia.webterm.viewmodels.ApiClientProjectsGateway
import wang.yaojia.webterm.viewmodels.ClientCertViewModel
import wang.yaojia.webterm.viewmodels.EnrollmentViewModel
import wang.yaojia.webterm.viewmodels.DiffViewModel
import wang.yaojia.webterm.viewmodels.HttpDiffFetcher
import wang.yaojia.webterm.viewmodels.PairingViewModel
import wang.yaojia.webterm.viewmodels.ProjectDetailViewModel
import wang.yaojia.webterm.viewmodels.ProjectsGateway
import wang.yaojia.webterm.wiring.AppEnvironment
import java.util.UUID

// ── Pairing (A19) ─────────────────────────────────────────────────────────────────────────────────────

/**
 * Hosts [PairingScreen] over a fresh [PairingViewModel]: the two-step probe uses the shared transports
 * (resolved lazily off `Main` inside the probe), and the tunnel cert-gate reads the device identity off
 * `Dispatchers.IO`. The screen does not self-bind, so this pane binds it to a lifecycle scope.
 */
@Composable
public fun PairingPane(
    env: AppEnvironment,
    onPaired: (Host) -> Unit,
    onImportCert: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val viewModel = remember(env) {
        PairingViewModel(
            hostStore = env.hostStore,
            prober = env.buildPairingProber(),
            hasDeviceCert = {
                withContext(Dispatchers.IO) {
                    runCatching { env.identityRepository.hasInstalledIdentity() }.getOrDefault(false)
                }
            },
        )
    }
    LaunchedEffect(viewModel) { viewModel.bind(this) }
    PairingScreen(viewModel = viewModel, onPaired = onPaired, onImportCert = onImportCert, modifier = modifier)
}

// ── Device certificate (A27) ────────────────────────────────────────────────────────────────────────

/** Hosts [ClientCertScreen] over a fresh [ClientCertViewModel] on the shared mTLS device identity. */
@Composable
public fun ClientCertPane(
    env: AppEnvironment,
    onBack: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val viewModel = remember(env) { ClientCertViewModel(repository = env.identityRepository) }
    ClientCertScreen(viewModel = viewModel, onBack = onBack, modifier = modifier)
}

// ── Device enrollment (B4, zero-.p12 auto-cert) ─────────────────────────────────────────────────────

/**
 * Hosts [EnrollmentScreen] over a fresh [EnrollmentViewModel] (mirrors iOS `AppCoordinator`'s
 * `makeEnrollmentViewModel`). The enroll flow and the installed-summary read run OFF `Main`
 * (`Dispatchers.IO`) — resolving the enroller resolves the lazy shared client (keystore/TLS I/O) and the
 * enroll itself does hardware-keygen + network. The typed control-plane URL builds the enroller per attempt.
 */
@Composable
public fun EnrollmentPane(
    env: AppEnvironment,
    onBack: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val viewModel = remember(env) {
        EnrollmentViewModel(
            enrollOperation = { password, subdomain, deviceName, controlPlaneUrl ->
                withContext(Dispatchers.IO) {
                    env.enrollmentFlowFactory.create(controlPlaneUrl).enroll(password, subdomain, deviceName)
                }
            },
            loadSummary = {
                withContext(Dispatchers.IO) {
                    runCatching { env.identityRepository.currentSummary() }.getOrNull()
                }
            },
            defaultDeviceName = android.os.Build.MODEL ?: "",
        )
    }
    EnrollmentScreen(viewModel = viewModel, onBack = onBack, modifier = modifier)
}

// ── Project detail (A23) ──────────────────────────────────────────────────────────────────────────────

/**
 * The project-detail destination: resolves [hostId] → its per-host projects gateway, then renders
 * [ProjectDetailContent]. "在此启动 Claude" spawns a new session in the project cwd.
 */
@Composable
public fun ProjectDetailPane(
    env: AppEnvironment,
    hostId: String?,
    path: String?,
    navController: NavHostController,
    modifier: Modifier = Modifier,
) {
    if (hostId == null || path == null) {
        MessagePane("无效的项目。", modifier)
        return
    }
    val host by produceState<Host?>(initialValue = null, key1 = hostId) {
        value = runCatchingCancellable { env.hostStore.loadAll().firstOrNull { it.id == hostId } }.getOrNull()
    }
    val resolved = host
    if (resolved == null) {
        MessagePane("正在打开项目…", modifier)
        return
    }
    val gateway = remember(resolved) { ApiClientProjectsGateway(env.apiClientFactory.create(resolved.endpoint)) }
    ProjectDetailContent(
        gateway = gateway,
        path = path,
        onBack = { navController.popBackStack() },
        onOpenClaude = { cwd -> navController.navigate(newTerminalRoute(resolved.id, cwd)) },
        onViewDiff = { diffPath -> navController.navigate(diffRoute(resolved.id, diffPath)) },
        modifier = modifier,
    )
}

/** Shared project-detail body — reused by the stacked destination and the tablet detail pane. */
@Composable
public fun ProjectDetailContent(
    gateway: ProjectsGateway,
    path: String,
    onBack: () -> Unit,
    onOpenClaude: (String) -> Unit,
    modifier: Modifier = Modifier,
    onViewDiff: (String) -> Unit = {},
) {
    val viewModel = remember(gateway, path) { ProjectDetailViewModel.forGateway(gateway, path) }
    ProjectDetailScreen(viewModel = viewModel, onBack = onBack, onOpenClaude = onOpenClaude, modifier = modifier, onViewDiff = onViewDiff)
}

// ── Diff viewer (A24) ─────────────────────────────────────────────────────────────────────────────────

/**
 * The read-only git-diff destination (A24): resolves [hostId] → the RO diff fetcher over the shared
 * [HttpTransport][wang.yaojia.webterm.wire.HttpTransport]. Composed for completeness; no inbound link is
 * wired yet (the project-detail screen exposes no "view diff" callback to connect).
 */
@Composable
public fun DiffPane(
    env: AppEnvironment,
    hostId: String?,
    path: String?,
    onBack: () -> Unit,
    modifier: Modifier = Modifier,
) {
    if (hostId == null || path == null) {
        MessagePane("无效的路径。", modifier)
        return
    }
    val host by produceState<Host?>(initialValue = null, key1 = hostId) {
        value = runCatchingCancellable { env.hostStore.loadAll().firstOrNull { it.id == hostId } }.getOrNull()
    }
    val resolved = host
    if (resolved == null) {
        MessagePane("正在加载 diff…", modifier)
        return
    }
    val viewModel = remember(resolved, path) {
        DiffViewModel(
            fetcher = HttpDiffFetcher(resolved.endpoint, env.httpTransport),
            path = path,
            // Guarded git-write flows through :api-client's single Origin-stamping point (plan §Security).
            writer = ApiClientGitWriteGateway(env.apiClientFactory.create(resolved.endpoint)),
        )
    }
    DiffScreen(viewModel = viewModel, modifier = modifier, onBack = onBack)
}

// ── Push-gate deep link (A30) ─────────────────────────────────────────────────────────────────────────

/**
 * The push-gate destination: a valid session id arrives with NO host (payload minimization, §4.5), so this
 * best-effort scans the paired hosts' live sessions for a match and redirects to that host's terminal
 * (which surfaces the held gate); if none matches, it falls back to the sessions chooser. All resolution
 * runs off `Main`; device-QA.
 */
@Composable
public fun GatePane(
    env: AppEnvironment,
    sessionArg: String?,
    navController: NavHostController,
    modifier: Modifier = Modifier,
) {
    val sessionId = NavRoutes.attachSessionId(sessionArg)
    LaunchedEffect(sessionId) {
        val route = gateTargetRoute(env, sessionId)
        runCatching {
            navController.navigate(route) {
                popUpTo(NavRoutes.SESSIONS) { inclusive = false }
                launchSingleTop = true
            }
        }
    }
    MessagePane("正在定位会话…", modifier)
}

/**
 * Resolve where a push-gate deep link should land: the terminal of the host that currently owns
 * [sessionId] (which surfaces the held gate), or the sessions chooser when the id is missing/invalid or
 * no paired host has it.
 */
private suspend fun gateTargetRoute(env: AppEnvironment, sessionId: String?): String {
    if (sessionId == null) return NavRoutes.SESSIONS
    val uuid = runCatching { UUID.fromString(sessionId) }.getOrNull() ?: return NavRoutes.SESSIONS
    val hostId = resolveHostForSession(env, uuid) ?: return NavRoutes.SESSIONS
    return NavRoutes.terminal(hostId, sessionId)
}

/** Scan paired hosts' `GET /live-sessions` for [id]; return the first host that has it (off `Main`). */
private suspend fun resolveHostForSession(env: AppEnvironment, id: UUID): String? =
    withContext(Dispatchers.IO) {
        for (host in runCatchingCancellable { env.hostStore.loadAll() }.getOrDefault(emptyList())) {
            val hasIt = runCatchingCancellable {
                env.apiClientFactory.create(host.endpoint).liveSessions().any { it.id == id }
            }.getOrDefault(false)
            if (hasIt) return@withContext host.id
        }
        null
    }

// ── Shared placeholders ───────────────────────────────────────────────────────────────────────────────

/** Centered informational text for the tablet detail pane's empty state. */
@Composable
public fun DetailPlaceholder(text: String, modifier: Modifier = Modifier) {
    CenteredMessage(text, modifier)
}

/** Centered informational text for a loading / invalid-argument pane. */
@Composable
public fun MessagePane(text: String, modifier: Modifier = Modifier) {
    CenteredMessage(text, modifier)
}

@Composable
private fun CenteredMessage(text: String, modifier: Modifier) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            text = text,
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            textAlign = TextAlign.Center,
            modifier = Modifier.padding(Spacing.xxl24),
        )
    }
}
