import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'settings_page.dart';

// Appends [segment] to the path of [base], preserving any existing context path.
// Examples:
//   jenkinsUri('https://host', 'login')           → https://host/login
//   jenkinsUri('https://host/', 'login')           → https://host/login
//   jenkinsUri('https://host/jenkins', 'login')    → https://host/jenkins/login
//   jenkinsUri('https://host/jenkins/', 'login')   → https://host/jenkins/login
Uri jenkinsUri(String baseUrl, String segment) {
  final base = Uri.parse(baseUrl.trim());
  final basePath = base.path.endsWith('/')
      ? base.path.substring(0, base.path.length - 1)
      : base.path;
  return Uri(
    scheme: base.scheme,
    host: base.host,
    port: base.hasPort ? base.port : null,
    path: '$basePath/$segment',
  );
}

// Parses the raw JSON string from /whoAmI/api/json and returns a result if
// the response indicates a logged-in (non-anonymous) user.
JenkinsWebLoginResult? parseWhoAmIResult(String jsonText) {
  try {
    final map = jsonDecode(jsonText) as Map<String, dynamic>;
    final anonymous = map['anonymous'] as bool? ?? true;
    if (anonymous) return null;
    final username = map['id'] as String? ?? '';
    if (username.isEmpty) return null;
    return JenkinsWebLoginResult(
      username: username,
      displayName: map['fullName'] as String?,
    );
  } catch (_) {
    return null;
  }
}

// Builds a JS probe that fetches the whoAmI path relative to the current origin.
// [probePath] must already be a safe literal (validated via Uri construction).
String buildProbeJs(String probePath) {
  final encodedPath = jsonEncode(probePath);
  return '''
(async () => {
  try {
    const res = await fetch($encodedPath, { credentials: 'same-origin' });
    if (!res.ok) return null;
    return await res.text();
  } catch (e) {
    return null;
  }
})()
''';
}

// Parsed crumb from /crumbIssuer/api/json.
class JenkinsCrumb {
  const JenkinsCrumb({required this.crumbRequestField, required this.crumb});

  final String crumbRequestField;
  final String crumb;
}

// Parsed generated API token value from the token generate endpoint.
class JenkinsGeneratedToken {
  const JenkinsGeneratedToken({required this.tokenValue});

  final String tokenValue;
}

// Parses the raw JSON from /crumbIssuer/api/json. Returns null on failure.
JenkinsCrumb? parseCrumbResult(String jsonText) {
  try {
    final map = jsonDecode(jsonText) as Map<String, dynamic>;
    final field = map['crumbRequestField'] as String? ?? '';
    final crumb = map['crumb'] as String? ?? '';
    if (field.isEmpty || crumb.isEmpty) return null;
    return JenkinsCrumb(crumbRequestField: field, crumb: crumb);
  } catch (_) {
    return null;
  }
}

// Parses the raw JSON from the token generate endpoint.
// Jenkins returns {"status":"ok","data":{"tokenName":"...","tokenUuid":"...","tokenValue":"..."}}
JenkinsGeneratedToken? parseTokenResult(String jsonText) {
  try {
    final map = jsonDecode(jsonText) as Map<String, dynamic>;
    if (map['status'] != 'ok') return null;
    final data = map['data'];
    if (data is! Map<String, dynamic>) return null;
    final tokenValue = data['tokenValue'] as String? ?? '';
    if (tokenValue.isEmpty) return null;
    return JenkinsGeneratedToken(tokenValue: tokenValue);
  } catch (_) {
    return null;
  }
}

// Builds a JS sequence that:
//   1. Fetches /crumbIssuer/api/json with same-origin credentials.
//   2. POSTs to the token generate endpoint using the crumb header.
// Returns JSON string "{\"crumb\":...,\"token\":...}" on success, null on failure.
// [crumbPath] and [tokenPath] must be safe literals from Uri construction.
// [username] must be percent-encoded before being passed to this function.
// [tokenName] is a non-secret display label.
String buildTokenBridgeJs(
  String crumbPath,
  String tokenPath,
  String tokenName,
) {
  final encodedCrumbPath = jsonEncode(crumbPath);
  final encodedTokenPath = jsonEncode(tokenPath);
  final encodedTokenName = jsonEncode(tokenName);
  return '''
(async () => {
  try {
    const crumbRes = await fetch($encodedCrumbPath, { credentials: 'same-origin' });
    if (!crumbRes.ok) return null;
    const crumbJson = await crumbRes.text();

    let crumbField, crumbValue;
    try {
      const c = JSON.parse(crumbJson);
      crumbField = c.crumbRequestField;
      crumbValue = c.crumb;
    } catch (_) { return null; }
    if (!crumbField || !crumbValue) return null;

    const body = new URLSearchParams({ newTokenName: $encodedTokenName });
    const tokenRes = await fetch($encodedTokenPath, {
      method: 'POST',
      credentials: 'same-origin',
      headers: { [crumbField]: crumbValue, 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    });
    if (!tokenRes.ok) return null;
    const tokenJson = await tokenRes.text();
    return JSON.stringify({ crumb: crumbJson, token: tokenJson });
  } catch (e) {
    return null;
  }
})()
''';
}

// Result of the full crumb+token bridge: crumb and generated token value.
class JenkinsBridgeTokenResult {
  const JenkinsBridgeTokenResult({
    required this.crumb,
    required this.token,
  });

  final JenkinsCrumb crumb;
  final JenkinsGeneratedToken token;
}

// Parses the composite JSON produced by [buildTokenBridgeJs].
JenkinsBridgeTokenResult? parseBridgeTokenResult(String compositeJson) {
  try {
    final outer = jsonDecode(compositeJson) as Map<String, dynamic>;
    final crumbJson = outer['crumb'] as String? ?? '';
    final tokenJson = outer['token'] as String? ?? '';
    final crumb = parseCrumbResult(crumbJson);
    final token = parseTokenResult(tokenJson);
    if (crumb == null || token == null) return null;
    return JenkinsBridgeTokenResult(crumb: crumb, token: token);
  } catch (_) {
    return null;
  }
}

class JenkinsWebLoginPage extends StatefulWidget {
  const JenkinsWebLoginPage({super.key, required this.baseUrl});

  final String baseUrl;

  @override
  State<JenkinsWebLoginPage> createState() => _JenkinsWebLoginPageState();
}

class _JenkinsWebLoginPageState extends State<JenkinsWebLoginPage> {
  late final WebViewController _controller;
  late final String _probePath;
  late final String _crumbPath;
  bool _isLoading = true;
  bool _issuingToken = false;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    final loginUri = jenkinsUri(widget.baseUrl, 'login');
    _probePath = jenkinsUri(widget.baseUrl, 'whoAmI/api/json').path;
    _crumbPath = jenkinsUri(widget.baseUrl, 'crumbIssuer/api/json').path;

    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (_) {
            if (mounted) setState(() => _isLoading = true);
          },
          onPageFinished: (_) {
            if (mounted) setState(() => _isLoading = false);
            _runProbe();
          },
          onWebResourceError: (error) {
            if (mounted) {
              setState(() {
                _isLoading = false;
                _errorMessage = error.description;
              });
            }
          },
        ),
      )
      ..loadRequest(loginUri);
  }

  Future<void> _runProbe() async {
    if (_issuingToken) return;
    try {
      final raw = await _controller.runJavaScriptReturningResult(
        buildProbeJs(_probePath),
      );
      final jsonText = raw is String ? raw : raw.toString();
      if (jsonText == 'null') return;
      final whoAmI = parseWhoAmIResult(jsonText);
      if (whoAmI == null || !mounted) return;

      // Login detected: issue crumb + API token via same-origin bridge.
      setState(() => _issuingToken = true);
      await _issueToken(whoAmI);
    } catch (_) {
      // Probe failure is non-fatal; user may need more navigation steps.
      if (mounted) setState(() => _issuingToken = false);
    }
  }

  Future<void> _issueToken(JenkinsWebLoginResult whoAmI) async {
    try {
      final encodedUser = Uri.encodeComponent(whoAmI.username);
      final tokenPath = jenkinsUri(
        widget.baseUrl,
        'user/$encodedUser/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken',
      ).path;
      // Token name is a non-secret display label; no credential in it.
      final tokenName =
          'AppSok macOS ${DateTime.now().millisecondsSinceEpoch}';

      final raw = await _controller.runJavaScriptReturningResult(
        buildTokenBridgeJs(_crumbPath, tokenPath, tokenName),
      );
      final compositeJson = raw is String ? raw : raw.toString();

      if (!mounted) return;

      if (compositeJson == 'null') {
        // Token bridge failed — return whoAmI-only result so caller can decide.
        Navigator.of(context).pop(whoAmI);
        return;
      }

      final bridgeResult = parseBridgeTokenResult(compositeJson);
      if (bridgeResult == null) {
        Navigator.of(context).pop(whoAmI);
        return;
      }

      Navigator.of(context).pop(
        JenkinsWebLoginResult(
          username: whoAmI.username,
          displayName: whoAmI.displayName,
          apiToken: bridgeResult.token.tokenValue,
        ),
      );
    } catch (_) {
      if (mounted) {
        Navigator.of(context).pop(whoAmI);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Jenkins 로그인'),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () => Navigator.of(context).pop(),
        ),
        actions: [
          if (_isLoading || _issuingToken)
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Center(
                child: SizedBox(
                  width: 18,
                  height: 18,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
              ),
            ),
        ],
      ),
      body: Stack(
        children: [
          WebViewWidget(controller: _controller),
          if (_errorMessage != null)
            Center(
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(Icons.error_outline, size: 48),
                    const SizedBox(height: 12),
                    Text(_errorMessage!),
                    const SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: () {
                        setState(() => _errorMessage = null);
                        _controller.reload();
                      },
                      child: const Text('다시 시도'),
                    ),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}
