# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

"""
Simplified Okta OAuth 2.0 PKCE Authentication for CloudZero Operations CLI.

This module provides a streamlined implementation of the OAuth 2.0 PKCE flow
for authenticating with Okta.
"""

import base64
import hashlib
import platform
import secrets
import threading
import webbrowser
from datetime import UTC, datetime
from urllib.parse import urlencode, urlparse

import httpx
from flask import Flask, request
from markupsafe import escape
from rich.console import Console
from werkzeug.serving import make_server

from src import __version__
from src.settings import Settings

from .token_storage import TokenStorage, TokenStorageError, get_token_storage
from .utils import (
    add_token_metadata,
    add_userinfo_metadata,
    extract_access_token,
    extract_refresh_token,
    is_token_valid,
    is_userinfo_valid,
)

# Flask app for OAuth callback
app = Flask(__name__)
server_token = {}
shutdown_event = threading.Event()

# Rich console for formatted output
console = Console()


@app.route("/callback")
def callback():
    """Handle OAuth callback from Okta on port 8042."""
    code = request.args.get("code")
    error = request.args.get("error")
    error_description = request.args.get("error_description", "No description provided")

    if error:
        server_token["error"] = f"{error}: {error_description}"
        shutdown_event.set()
        return (
            f"""
        <html>
        <head><title>Authentication Failed</title></head>
        <body style="background: #000000; color: #ffffff; text-align: center; padding: 80px 40px;
                     font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
            <div style="font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 0.72rem;
                        letter-spacing: 0.06em; text-transform: uppercase; color: #B8FDFF; margin-bottom: 14px;">
                CloudZero &middot; CZOPS CLI</div>
            <h2 style="font-weight: 700; letter-spacing: -0.03em; margin: 0 0 24px;">
                Authentication <span style="background: #FE552E; border-radius: 0 12px 0 12px; padding: 0 0.3em;">failed</span></h2>
            <p style="color: #EAE7E1;">Error: {escape(error)}</p>
            <p style="color: #EAE7E1;">Description: {escape(error_description)}</p>
            <p style="color: rgba(255, 255, 255, 0.6);">Please close this window and check the terminal for more information.</p>
        </body>
        </html>
        """,
            400,
        )

    if not code:
        server_token["error"] = "No authorization code provided"
        shutdown_event.set()
        return "No authorization code provided", 400

    server_token["code"] = code
    shutdown_event.set()
    return (
        """
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Authentication Successful</title>
        <style>
            /* CloudZero visual identity (cz-standards/design-visual-identity):
               Zero Black background (approved with white logo + orange mark), Inter for
               headlines, Geist Mono for callouts/captions, Insight Ice / Warm Sand accents,
               Deep Teal text highlight with the brand's R0/R12 clipped corners, 1px rules. */
            :root {
                --cz-orange: #FE552E;
                --cz-insight-ice: #B8FDFF;
                --cz-signal-blue: #4599AC;
                --cz-deep-teal: #026681;
                --cz-warm-sand: #EAE7E1;
                --cz-charcoal: #403E3F;
                --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
                --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
            }
            body {
                margin: 0;
                padding: 0;
                background: #000000;
                font-family: var(--font-sans);
                color: #ffffff;
                overflow: hidden;
                display: flex;
                flex-direction: column;
                justify-content: center;
                align-items: center;
                height: 100vh;
            }
            #canvas {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                z-index: 1;
            }
            .content {
                position: relative;
                z-index: 2;
                text-align: center;
                width: 100%;
                max-width: 640px;
                padding: 0 40px;
                pointer-events: none;
            }
            .eyebrow {
                font-family: var(--font-mono);
                font-weight: 500;
                font-size: 0.72rem;
                letter-spacing: 0.06em;
                text-transform: uppercase;
                color: var(--cz-insight-ice);
                margin-bottom: 14px;
            }
            .message {
                font-family: var(--font-sans);
                font-size: 2rem;
                font-weight: 700;
                line-height: 1.3;
                letter-spacing: -0.03em;
                margin-bottom: 28px;
                color: #ffffff;
            }
            /* Brand text highlight (dark variant): Deep Teal behind white text,
               corners R0 top-left/bottom-right, R12 top-right/bottom-left */
            .highlight {
                background: var(--cz-deep-teal);
                border-radius: 0 12px 0 12px;
                padding: 0 0.3em;
            }
            .divider {
                width: 100%;
                height: 1px;
                background: linear-gradient(90deg,
                    transparent 0%,
                    rgba(255, 255, 255, 0.2) 20%,
                    rgba(255, 255, 255, 0.4) 50%,
                    rgba(255, 255, 255, 0.2) 80%,
                    transparent 100%);
                margin: 0 auto;
            }
            .countdown {
                margin-top: 26px;
                font-family: var(--font-mono);
                font-size: 0.85rem;
                letter-spacing: 0.06em;
                color: var(--cz-warm-sand);
            }
            /* Card silhouette per brand: one clipped corner, soft radii elsewhere */
            .pause-control {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 3;
                display: flex;
                align-items: center;
                gap: 8px;
                background: var(--cz-charcoal);
                padding: 10px 16px;
                border-radius: 0 12px 12px 12px;
                border: 1px solid rgba(255, 255, 255, 0.2);
                cursor: pointer;
                user-select: none;
            }
            .pause-control:hover {
                border-color: var(--cz-insight-ice);
            }
            .pause-control input[type="checkbox"] {
                width: 18px;
                height: 18px;
                cursor: pointer;
                accent-color: var(--cz-insight-ice);
            }
            .pause-control label {
                font-family: var(--font-mono);
                font-size: 0.78rem;
                letter-spacing: 0.06em;
                color: #ffffff;
                cursor: pointer;
            }
            .version-info {
                position: fixed;
                bottom: 20px;
                left: 20px;
                z-index: 3;
                font-family: var(--font-mono);
                font-size: 0.75rem;
                letter-spacing: 0.06em;
                color: rgba(255, 255, 255, 0.4);
            }
            .version-info a {
                color: var(--cz-insight-ice);
                text-decoration: none;
            }
            .version-info a:hover {
                color: #ffffff;
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <canvas id="canvas"></canvas>
        <div class="content">
            <div class="eyebrow">CloudZero &middot; CZOPS CLI</div>
            <div class="message">Authentication <span class="highlight">successful</span></div>
            <div class="divider"></div>
            <div class="countdown">Window closes in <span id="timer">30</span> seconds</div>
        </div>
        <div class="pause-control" onclick="document.getElementById('pauseCheckbox').click()">
            <input type="checkbox" id="pauseCheckbox" onclick="event.stopPropagation()">
            <label for="pauseCheckbox">Pause timer</label>
        </div>
        <div class="version-info">
            CZOPS v"""
        + __version__
        + """ |
            <a href="https://github.com/Cloudzero/cz-ops-cli/releases/tag/v"""
        + __version__
        + """" target="_blank">Release Notes</a>
        </div>
        <script>
            // Murmuration of Mouse Pointer Sparrows
            //
            // Starling-inspired flocking: real murmurations coordinate with a fixed number
            // of TOPOLOGICAL neighbors (~7 nearest, regardless of distance), never hover,
            // and fold back at the edges of the flock volume instead of teleporting —
            // that's what produces the rippling sheet shapes. A few CloudZero brand-mark
            // "leader" birds (4x scale) wander independently and pull followers with them.
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            const DPR = Math.min(window.devicePixelRatio || 1, 2);
            let W = window.innerWidth, H = window.innerHeight;

            function sizeCanvas() {
                W = window.innerWidth; H = window.innerHeight;
                canvas.width = W * DPR; canvas.height = H * DPR;
                canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
                ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
            }
            sizeCanvas();

            // CloudZero brand mark (CZ Orange), rendered as the leader-bird sprite
            const CZ_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 155.7172 150.97">'
                + '<path fill="#FE552E" d="m126.5906,13.7172L102.8321.0007l-24.9733,14.4181L52.8855,0l-27.9122,16.115.0007,28.8369L0,59.37v27.4345c0,20.745,11.1605,40.0761,29.1266,50.4489l23.7589,13.7166,24.9733-14.4181,24.9733,14.4181,27.9119-16.1143v-28.8369l24.9733-14.4181v-27.4345c0-20.745-11.1608-40.0761-29.1266-50.4489Zm-26.6971,49.0465v25.4432l-22.0347,12.722-22.0347-12.722-.0003-25.4432,22.035-12.7226,22.0347,12.7226Zm-49.9466,25.8424l-19.4418,11.2246v-22.8477l19.4416-11.2249.0002,22.8481Zm-16.8486,15.716l19.4416-11.2245,19.7873,11.4238-19.4417,11.225-18.1168-10.4598-1.6704-.9645Zm50.2922.1993l19.7873-11.4238,19.4414,11.2243-19.787,11.4244-19.4417-11.225Zm-8.1245-59.7694l-19.7875,11.4243v-22.449l19.7875-11.4242v22.4489Zm-27.5666,16.3148l-19.7871,11.4245-19.4417-11.2249,19.7878-11.424,10.7497,6.2062,8.6913,5.0181Zm5.1862-31.8312l-19.4415-11.2244,19.4415-11.2246,19.4417,11.2245-19.4417,11.2245Zm48.5593-.8005l-18.0547-10.4238,19.4419-11.2246,20.82,12.0204c16.1535,9.3262,26.188,26.7066,26.188,45.3591v24.0408l-19.4415,11.2244v-20.8471c0-20.6221-11.0943-39.8384-28.9538-50.1491Zm-45.9664,91.8029l19.7875-11.4247v22.4495l-19.7875,11.4242v-22.449Zm49.9466,0l19.4418-11.2251v22.4491l-19.4418,11.2249v-22.4489Z"/></svg>';
            const CZ_ICON = new Image();
            let czIconReady = false;
            CZ_ICON.onload = function() { czIconReady = true; };
            CZ_ICON.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(CZ_ICON_SVG);

            const particles = [];
            const particleCount = 900;
            const LEADER_COUNT = 4;       // CZ brand-mark leaders in the initial flock
            const MAX_LEADERS = 8;
            const MAX_BIRDS = 2200;       // cap click-spawning before frame rate suffers
            const PERCEPTION = 90;        // vision radius (px)
            const TOPO_NEIGHBORS = 7;     // starlings coordinate with ~7 nearest neighbors
            const SEP_RADIUS = 24;        // personal space
            const EDGE_MARGIN = 90;       // soft turn-back zone at screen edges
            const LEADER_PULL_RADIUS = 320; // leaders attract followers inside this range
            const GRID = PERCEPTION;      // spatial-hash cell size
            const time = { value: 0 };

            // Mouse tracking
            const mouse = {
                x: W / 2,
                y: H / 2,
                isMoving: false,
                lastMoveTime: 0
            };

            // Track mouse movement
            canvas.addEventListener('mousemove', (e) => {
                mouse.x = e.clientX;
                mouse.y = e.clientY;
                mouse.isMoving = true;
                mouse.lastMoveTime = Date.now();
            });

            // Check if mouse has stopped moving
            setInterval(() => {
                if (Date.now() - mouse.lastMoveTime > 300) {
                    mouse.isMoving = false;
                }
            }, 100);

            // Add birds when mouse button is held down
            let mousePressed = false;
            let spawnInterval = null;

            canvas.addEventListener('mousedown', (e) => {
                mousePressed = true;
                mouse.x = e.clientX;
                mouse.y = e.clientY;

                // Spawn birds continuously while mouse is held down
                spawnInterval = setInterval(() => {
                    if (mousePressed) {
                        // Add 5 birds at a time around the mouse position; occasionally one
                        // is a CZ brand-mark leader that the flock will follow
                        for (let i = 0; i < 5 && particles.length < MAX_BIRDS; i++) {
                            const spawnLeader = Math.random() < 0.02 && leaderCount() < MAX_LEADERS;
                            const newBird = new Sparrow(particles.length, spawnLeader);
                            // Spawn near mouse with slight random offset
                            const angle = Math.random() * Math.PI * 2;
                            const radius = Math.random() * 30;
                            newBird.x = mouse.x + Math.cos(angle) * radius;
                            newBird.y = mouse.y + Math.sin(angle) * radius;
                            // Give them initial velocity away from spawn point
                            newBird.vx = Math.cos(angle) * 2;
                            newBird.vy = Math.sin(angle) * 2;
                            particles.push(newBird);
                        }
                    }
                }, 100); // Spawn every 100ms
            });

            canvas.addEventListener('mouseup', () => {
                mousePressed = false;
                if (spawnInterval) {
                    clearInterval(spawnInterval);
                    spawnInterval = null;
                }
            });

            canvas.addEventListener('mouseleave', () => {
                mousePressed = false;
                if (spawnInterval) {
                    clearInterval(spawnInterval);
                    spawnInterval = null;
                }
            });

            // Spatial hash grid — neighbor queries drop from O(n^2) to O(n * local density)
            let grid = new Map();
            function buildGrid() {
                grid = new Map();
                for (const p of particles) {
                    const key = ((p.x / GRID) | 0) + ',' + ((p.y / GRID) | 0);
                    let cell = grid.get(key);
                    if (!cell) { cell = []; grid.set(key, cell); }
                    cell.push(p);
                }
            }
            function nearbyParticles(p) {
                const cx = (p.x / GRID) | 0, cy = (p.y / GRID) | 0;
                const found = [];
                for (let gx = cx - 1; gx <= cx + 1; gx++) {
                    for (let gy = cy - 1; gy <= cy + 1; gy++) {
                        const cell = grid.get(gx + ',' + gy);
                        if (cell) for (const other of cell) {
                            if (other === p) continue;
                            const dx = other.x - p.x, dy = other.y - p.y;
                            const dist = Math.sqrt(dx * dx + dy * dy);
                            if (dist > 0 && dist < PERCEPTION) found.push({ other, dx, dy, dist });
                        }
                    }
                }
                return found;
            }

            const leaders = [];
            function leaderCount() { return leaders.length; }

            class Sparrow {
                constructor(index, isLeader = false) {
                    this.x = Math.random() * W;
                    this.y = Math.random() * H;
                    this.vx = (Math.random() - 0.5) * 2;
                    this.vy = (Math.random() - 0.5) * 2;
                    this.isLeader = isLeader;
                    if (isLeader) {
                        // The CZ brand mark flies steadier and a touch slower, so the
                        // flock can catch and shadow it
                        this.maxSpeed = 2.9;
                        this.minSpeed = 1.8;
                        this.maxForce = 0.05;
                        this.wanderAngle = Math.random() * Math.PI * 2;
                        leaders.push(this);
                    } else {
                        // Real flocks aren't uniform: individual cruising speeds vary,
                        // and a bird can never hover — it stalls below minSpeed
                        this.maxSpeed = 2.8 + Math.random() * 0.9;
                        this.minSpeed = 1.4;
                        this.maxForce = 0.06;
                    }
                }

                // Steer toward a desired direction (dx, dy), clamped to maxForce * weight
                steer(dx, dy, weight) {
                    const mag = Math.sqrt(dx * dx + dy * dy);
                    if (mag === 0) return;
                    let fx = (dx / mag) * this.maxSpeed - this.vx;
                    let fy = (dy / mag) * this.maxSpeed - this.vy;
                    const fmag = Math.sqrt(fx * fx + fy * fy);
                    const limit = this.maxForce * weight;
                    if (fmag > limit) { fx = (fx / fmag) * limit; fy = (fy / fmag) * limit; }
                    this.vx += fx;
                    this.vy += fy;
                }

                update() {
                    if (this.isLeader) {
                        this.updateLeader();
                    } else {
                        this.updateFollower();
                    }

                    // Clamp speed between stall and sprint — birds never hover
                    const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
                    if (speed > this.maxSpeed) {
                        this.vx = (this.vx / speed) * this.maxSpeed;
                        this.vy = (this.vy / speed) * this.maxSpeed;
                    } else if (speed > 0 && speed < this.minSpeed) {
                        this.vx = (this.vx / speed) * this.minSpeed;
                        this.vy = (this.vy / speed) * this.minSpeed;
                    }

                    this.x += this.vx;
                    this.y += this.vy;

                    // Hard clamp as a backstop; avoidEdges() should turn birds first.
                    // No wraparound: flocks fold back at boundaries, which is what
                    // creates murmuration's characteristic rippling sheets.
                    if (this.x < 0) { this.x = 0; this.vx = Math.abs(this.vx); }
                    if (this.x > W) { this.x = W; this.vx = -Math.abs(this.vx); }
                    if (this.y < 0) { this.y = 0; this.vy = Math.abs(this.vy); }
                    if (this.y > H) { this.y = H; this.vy = -Math.abs(this.vy); }
                }

                avoidEdges(weight) {
                    let ex = 0, ey = 0;
                    if (this.x < EDGE_MARGIN) ex = 1 - this.x / EDGE_MARGIN;
                    else if (this.x > W - EDGE_MARGIN) ex = -(1 - (W - this.x) / EDGE_MARGIN);
                    if (this.y < EDGE_MARGIN) ey = 1 - this.y / EDGE_MARGIN;
                    else if (this.y > H - EDGE_MARGIN) ey = -(1 - (H - this.y) / EDGE_MARGIN);
                    if (ex !== 0 || ey !== 0) this.steer(ex, ey, weight);
                }

                followMouse(weight) {
                    if (!mouse.isMoving) return;
                    const dx = mouse.x - this.x, dy = mouse.y - this.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist === 0) return;
                    // Distance falloff: nearby birds react strongly, far birds barely —
                    // a global pull looks magnetic, not avian
                    this.steer(dx, dy, weight / (1 + dist / 300));
                }

                updateFollower() {
                    const neighbors = nearbyParticles(this);

                    // Topological interaction: react to the ~7 NEAREST neighbors, however
                    // close or far — the mechanism (Ballerini et al.) behind how real
                    // starling flocks stay coherent as density changes
                    neighbors.sort((a, b) => a.dist - b.dist);
                    const topo = neighbors.slice(0, TOPO_NEIGHBORS);

                    let alignX = 0, alignY = 0, cohX = 0, cohY = 0;
                    for (const n of topo) {
                        alignX += n.other.vx; alignY += n.other.vy;
                        cohX += n.dx; cohY += n.dy;
                    }
                    if (topo.length > 0) {
                        // Alignment dominates in real murmurations; cohesion is gentler
                        this.steer(alignX, alignY, 1.0);
                        this.steer(cohX, cohY, 0.55);
                    }

                    // Separation uses metric distance (collision avoidance is physical),
                    // weighted harder the closer the intruder
                    let sepX = 0, sepY = 0, sepCount = 0;
                    for (const n of neighbors) {
                        if (n.dist < SEP_RADIUS) {
                            sepX -= n.dx / (n.dist * n.dist);
                            sepY -= n.dy / (n.dist * n.dist);
                            sepCount++;
                        }
                    }
                    if (sepCount > 0) this.steer(sepX, sepY, 1.6);

                    // Follow the nearest CZ brand-mark leader in range
                    let nearest = null, nearestDist = LEADER_PULL_RADIUS;
                    for (const leader of leaders) {
                        const dx = leader.x - this.x, dy = leader.y - this.y;
                        const dist = Math.sqrt(dx * dx + dy * dy);
                        if (dist < nearestDist) { nearest = leader; nearestDist = dist; }
                    }
                    if (nearest) {
                        if (nearestDist > 50) {
                            // Draw toward the leader from range...
                            this.steer(nearest.x - this.x, nearest.y - this.y, 0.9);
                        } else {
                            // ...but hold a standoff — birds shadow a leader, they
                            // don't pile onto its back
                            this.steer(this.x - nearest.x, this.y - nearest.y, 0.8);
                        }
                        this.steer(nearest.vx, nearest.vy, 0.5); // shadow its heading
                    }

                    this.avoidEdges(2.0);
                    this.followMouse(2.0);

                    // Individual noise keeps the sheet shimmering instead of freezing
                    this.vx += (Math.random() - 0.5) * 0.04;
                    this.vy += (Math.random() - 0.5) * 0.04;
                }

                updateLeader() {
                    // Smoothly wandering heading — leaders carve the flock's path
                    this.wanderAngle += (Math.random() - 0.5) * 0.18;
                    this.steer(Math.cos(this.wanderAngle), Math.sin(this.wanderAngle), 0.6);

                    // Leaders spread out from each other so the flock forms lobes
                    for (const other of leaders) {
                        if (other === this) continue;
                        const dx = this.x - other.x, dy = this.y - other.y;
                        const dist = Math.sqrt(dx * dx + dy * dy);
                        if (dist > 0 && dist < 260) this.steer(dx, dy, 1.2);
                    }

                    this.avoidEdges(2.5);
                    this.followMouse(1.0);
                }

                draw() {
                    // Calculate angle based on velocity
                    const angle = Math.atan2(this.vy, this.vx);

                    ctx.save();
                    ctx.translate(this.x, this.y);
                    ctx.rotate(angle);

                    if (this.isLeader) {
                        // CloudZero brand mark as a 4x bird (sprite is 9px long -> 36px),
                        // native icon ratio 155.7:151 preserved
                        if (czIconReady) {
                            ctx.drawImage(CZ_ICON, -18, -17.45, 36, 34.9);
                        } else {
                            // Icon not decoded yet: same arrow sprite at 4x as a stand-in
                            ctx.scale(4, 4);
                            ctx.beginPath();
                            ctx.moveTo(6, 0);
                            ctx.lineTo(-3, -3);
                            ctx.lineTo(-2, 0);
                            ctx.lineTo(-3, 3);
                            ctx.closePath();
                            ctx.fillStyle = '#FE552E';
                            ctx.fill();
                        }
                    } else {
                        // Draw black arrow/bird shape
                        ctx.beginPath();
                        ctx.moveTo(6, 0);           // Point forward
                        ctx.lineTo(-3, -3);         // Top wing
                        ctx.lineTo(-2, 0);          // Body
                        ctx.lineTo(-3, 3);          // Bottom wing
                        ctx.closePath();

                        ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
                        ctx.fill();
                    }

                    ctx.restore();
                }
            }

            // Initialize sparrows; a few random flock positions carry the CZ brand mark
            const leaderSlots = new Set();
            while (leaderSlots.size < LEADER_COUNT) {
                leaderSlots.add((Math.random() * particleCount) | 0);
            }
            for (let i = 0; i < particleCount; i++) {
                particles.push(new Sparrow(i, leaderSlots.has(i)));
            }

            // Animation loop
            function animate() {
                time.value++;

                // Clear canvas (no trail effect - just clear background)
                ctx.clearRect(0, 0, W, H);

                buildGrid();
                for (const particle of particles) particle.update();
                // Leaders draw last so the brand mark rides above the flock
                for (const particle of particles) if (!particle.isLeader) particle.draw();
                for (const leader of leaders) leader.draw();

                requestAnimationFrame(animate);
            }

            animate();

            // Countdown timer with pause functionality
            let seconds = 30;
            const timerElement = document.getElementById('timer');
            const pauseCheckbox = document.getElementById('pauseCheckbox');
            let closeAttempted = false;

            function tryCloseWindow() {
                if (closeAttempted) return;
                closeAttempted = true;

                // Try multiple close methods
                try { window.close(); } catch(e) {}
                try { self.close(); } catch(e) {}
                try { top.close(); } catch(e) {}

                // If window is still open after 500ms, show manual close message
                setTimeout(() => {
                    if (!window.closed) {
                        document.querySelector('.countdown').innerHTML =
                            '<span style="color: #B8FDFF;">✓ Authentication complete!</span><br>' +
                            '<span style="font-size: 0.85em; margin-top: 10px; display: inline-block;">' +
                            'You can safely close this browser tab and return to your terminal.</span>';
                        document.querySelector('.pause-control').style.display = 'none';
                    }
                }, 500);
            }

            function startCountdown() {
                const countdownInterval = setInterval(() => {
                    if (!pauseCheckbox.checked) {
                        seconds--;
                        timerElement.textContent = seconds;
                        if (seconds <= 0) {
                            clearInterval(countdownInterval);
                            tryCloseWindow();
                        }
                    }
                }, 1000);
            }

            // Start countdown
            startCountdown();

            // Update timer text when checkbox changes
            pauseCheckbox.addEventListener('change', (e) => {
                if (e.target.checked) {
                    timerElement.textContent = seconds + ' (paused)';
                } else {
                    timerElement.textContent = seconds;
                }
            });

            // Handle window resize
            window.addEventListener('resize', sizeCanvas);
        </script>
    </body>
    </html>
    """
    )


class OktaPKCEAuthenticator:
    """Simplified Okta PKCE authenticator with secure file-based storage."""

    def __init__(self, token_storage: TokenStorage | None = None):
        """Initialize the authenticator with settings and token storage.

        Args:
            token_storage: Optional custom token storage instance for testing
        """
        self.settings = Settings()
        self.client_id = self.settings.api_v4.auth["CLIENT_ID"]
        self.okta_domain = self.settings.api_v4.auth["OKTA_DOMAIN"]
        self.redirect_uri = self.settings.api_v4.auth["LOGIN_REDIRECT_URI"]

        # Okta endpoints - using default API authorization server
        self.authorization_endpoint = f"https://{self.okta_domain}/oauth2/default/v1/authorize"
        self.token_endpoint = f"https://{self.okta_domain}/oauth2/default/v1/token"
        self.userinfo_endpoint = f"https://{self.okta_domain}/oauth2/default/v1/userinfo"

        self.scopes = ["openid", "profile", "email", "offline_access", "groups"]

        # Initialize token storage (allows dependency injection for testing)
        self.token_storage = token_storage or get_token_storage()

    def authenticate(self, force=False, debug=False):
        """
        Perform PKCE authentication flow.

        Args:
            force: Force re-authentication even if valid token exists
            debug: Show debug information during authentication

        Returns:
            dict: Token response containing access_token, refresh_token, etc.
        """
        # Check for existing valid token
        if not force:
            existing_token = self.get_stored_token()
            if existing_token and self.is_token_valid(existing_token):
                return existing_token

        # Generate PKCE code verifier and challenge
        code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("utf-8").rstrip("=")
        code_challenge = (
            base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("utf-8")).digest()).decode("utf-8").rstrip("=")
        )

        # Generate state for CSRF protection
        state = secrets.token_urlsafe(32)

        # Build authorization URL
        auth_params = {
            "client_id": self.client_id,
            "response_type": "code",
            "scope": " ".join(self.scopes),
            "redirect_uri": self.redirect_uri,
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            # Request group claims in both userinfo and id_token
            "claims": '{"userinfo":{"groups":{"essential":true}},"id_token":{"groups":{"essential":true}}}',
        }
        uri = f"{self.authorization_endpoint}?{urlencode(auth_params)}"

        # Store the code verifier and state for later use
        server_token["verifier"] = code_verifier
        server_token["state"] = state

        if debug:
            print("\n🔐 Opening browser for authentication...")
            print(f"If the browser doesn't open automatically, visit:\n{uri}\n")

        # Show debug information for authorization parameters
        if debug:
            print("Authorization Request Parameters:")
            for key, value in auth_params.items():
                if key == "claims":
                    print(f"  {key}: {value}")
                else:
                    print(f"  {key}: {value[:50]}..." if len(str(value)) > 50 else f"  {key}: {value}")

        # Show debug information only when requested
        if debug:
            print("Debug Information:")
            print(f"  Client ID: {self.client_id}")
            print(f"  Redirect URI: {self.redirect_uri}")
            print(f"  Okta Domain: {self.okta_domain}")
            print(f"  Authorization Endpoint: {self.authorization_endpoint}")

        # Check for configuration mismatch (always show warnings)
        if self.redirect_uri and "8080" in self.redirect_uri:
            print("\n⚠️  WARNING: Your settings are using port 8080, but Okta app expects port 8042!")
            print("   Please run: czops configure --auth")
            print("   And update LOGIN_REDIRECT_URI to: http://127.0.0.1:8042/callback")

        print()

        # Try to open browser with enhanced error handling
        print("🔐 Opening browser for authentication...")
        browser_opened = webbrowser.open(uri)

        # Always display the URL for manual access (helps with Windows/WSL issues)
        console.print("\n[bold cyan]Authentication URL:[/bold cyan]")
        console.print(f"[link={uri}]{uri}[/link]")

        if browser_opened:
            console.print("\n[green]✓[/green] Browser opened automatically")
        else:
            console.print("\n[yellow]⚠ Browser did not open automatically[/yellow]")
            console.print("[yellow]Please copy the URL above and paste it into your browser[/yellow]")

            if platform.system() == "Windows":
                console.print("\n[dim]Windows tip: Right-click to copy the URL, or try running as Administrator[/dim]")

        console.print("\n[dim]Waiting for you to complete authentication in your browser...[/dim]")

        # Start local server to capture callback
        authorization_code = self._run_callback_server()

        if not authorization_code:
            raise AuthenticationError("Failed to get authorization code")

        # Exchange code for tokens
        token = self._exchange_code_for_token(authorization_code, server_token["verifier"])

        if debug:
            print("Received Token:")
            for k, v in token.items():
                if k in ["access_token", "refresh_token", "id_token"]:
                    print(f"  {k}: {'*' * 8} (hidden for security)")
                else:
                    print(f"  {k}: {v}")
            print()

        # Store the token
        self.store_token(token)

        # Fetch and cache user info
        self._fetch_and_cache_userinfo(extract_access_token(token))

        return token

    def _run_callback_server(self, timeout=300):
        """
        Run a local Flask server to capture the OAuth callback.

        Args:
            timeout: Maximum time to wait for callback in seconds

        Returns:
            str: Authorization code or None if failed
        """
        # Clear previous state but preserve verifier and state
        code_verifier = server_token.get("verifier")
        state = server_token.get("state")
        server_token.clear()
        if code_verifier:
            server_token["verifier"] = code_verifier
        if state:
            server_token["state"] = state
        shutdown_event.clear()

        # Extract host and port from redirect URI
        parsed_uri = urlparse(self.redirect_uri)
        host = parsed_uri.hostname or "127.0.0.1"
        port = parsed_uri.port or 8042  # Default to 8042 if not specified

        # Create server in a separate thread with logging disabled
        import logging

        log = logging.getLogger("werkzeug")
        log.setLevel(logging.ERROR)  # Suppress INFO logs from Flask/Werkzeug

        server = make_server(host, port, app, threaded=True)
        server_thread = threading.Thread(target=server.serve_forever)
        server_thread.daemon = True
        server_thread.start()

        # Wait for callback or timeout
        if not shutdown_event.wait(timeout):
            server.shutdown()
            raise AuthenticationError("Authentication timed out")

        server.shutdown()

        if "error" in server_token:
            raise AuthenticationError(f"Authentication failed: {server_token['error']}")

        return server_token.get("code")

    def _exchange_code_for_token(self, code, code_verifier):
        """
        Exchange authorization code for access and refresh tokens.

        Args:
            code: Authorization code from callback
            code_verifier: PKCE code verifier

        Returns:
            dict: Token response
        """
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": self.redirect_uri,
            "client_id": self.client_id,
            "code_verifier": code_verifier,
        }

        response = httpx.post(
            self.token_endpoint, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=30
        )

        if response.status_code != 200:
            raise AuthenticationError(f"Token exchange failed: {response.text}")

        token = response.json()

        # Add metadata using utility function
        enhanced_token = add_token_metadata(token)

        return enhanced_token

    def refresh_token(self, refresh_token):
        """
        Refresh an access token using a refresh token.

        Args:
            refresh_token: Refresh token to use

        Returns:
            dict: New token response
        """
        data = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": self.client_id,
        }

        response = httpx.post(
            self.token_endpoint, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=30
        )

        if response.status_code != 200:
            raise AuthenticationError(f"Token refresh failed: {response.text}")

        token = response.json()

        # Add metadata using utility function
        enhanced_token = add_token_metadata(token)

        # Store the refreshed token
        self.store_token(enhanced_token)

        # Refresh user info with new token
        self._fetch_and_cache_userinfo(extract_access_token(enhanced_token))

        return enhanced_token

    def get_user_info(self, access_token):
        """
        Get user information from Okta.

        Args:
            access_token: Valid access token

        Returns:
            dict: User information
        """
        response = httpx.get(self.userinfo_endpoint, headers={"Authorization": f"Bearer {access_token}"}, timeout=30)

        if response.status_code != 200:
            raise AuthenticationError(f"Failed to get user info: {response.text}")

        return response.json()

    def get_extended_user_info(self, access_token=None):
        """
        Get extended user information including groups and metadata from multiple sources.

        This method combines information from:
        1. Okta UserInfo endpoint (basic profile + groups if available)
        2. JWT token claims
        3. CloudZero backend user verification

        Args:
            access_token: Valid access token (optional, will get current if not provided)

        Returns:
            dict: Extended user information with groups, department, title, etc.
        """
        if not access_token:
            access_token = self.get_access_token()
            if not access_token:
                raise AuthenticationError("No valid access token available")

        # Get basic user info from Okta
        userinfo = self.get_user_info(access_token)

        # Store original Okta response for debugging
        userinfo["_debug_okta_raw_response"] = dict(userinfo)  # Make a copy for debugging

        # Try to get additional info from JWT tokens (both Access Token and ID Token)
        try:
            import base64
            import json

            # Decode Access Token payload to get additional claims
            parts = access_token.split(".")
            if len(parts) == 3:
                payload_data = parts[1] + "=" * (4 - len(parts[1]) % 4)  # Add padding
                payload = json.loads(base64.urlsafe_b64decode(payload_data))

                # Add Access Token JWT claims to userinfo if they exist
                access_token_claims = {
                    "uid": payload.get("uid"),
                    "groups_from_access_token": payload.get("groups", []),
                    "scp": payload.get("scp", []),
                    "auth_time": payload.get("auth_time"),
                    "iat": payload.get("iat"),
                    "exp": payload.get("exp"),
                }
                userinfo.update({k: v for k, v in access_token_claims.items() if v is not None})
        except Exception:
            # If JWT parsing fails, continue with basic userinfo
            pass

        # Also check ID Token for additional claims (especially groups)
        try:
            import base64
            import json

            # Get stored token to check for ID token
            stored_token = self.get_stored_token()
            if stored_token and "id_token" in stored_token:
                id_token = stored_token["id_token"]
                parts = id_token.split(".")
                if len(parts) == 3:
                    payload_data = parts[1] + "=" * (4 - len(parts[1]) % 4)  # Add padding
                    id_payload = json.loads(base64.urlsafe_b64decode(payload_data))

                    # Add ID Token JWT claims to userinfo if they exist
                    id_token_claims = {
                        "groups_from_id_token": id_payload.get("groups", []),
                        "id_token_uid": id_payload.get("uid"),
                        "id_token_sub": id_payload.get("sub"),
                        "id_token_aud": id_payload.get("aud"),
                        "id_token_iss": id_payload.get("iss"),
                        "id_token_exp": id_payload.get("exp"),
                        "id_token_iat": id_payload.get("iat"),
                    }
                    userinfo.update({k: v for k, v in id_token_claims.items() if v is not None})

                    # If ID token has groups, also add them to the general groups_from_jwt field
                    if id_payload.get("groups"):
                        userinfo["groups_from_jwt"] = id_payload.get("groups", [])
        except Exception:
            # If ID Token parsing fails, continue
            pass

        # Try to get additional metadata from CloudZero backend
        try:
            from src.api.authenticated import get_authenticated

            backend_info = get_authenticated("auth/verify", debug=False)

            # Merge backend user metadata
            backend_fields = {
                "user_groups": backend_info.get("user_groups", []),
                "user_department": backend_info.get("user_department"),
                "user_title": backend_info.get("user_title"),
                "user_full_name": backend_info.get("user_full_name"),
                "backend_user_id": backend_info.get("user_id"),
            }
            userinfo.update({k: v for k, v in backend_fields.items() if v is not None})
        except Exception:
            # If backend call fails, continue with Okta data only
            pass

        return userinfo

    def _fetch_and_cache_userinfo(self, access_token):
        """
        Fetch user info from Okta and cache it.

        Args:
            access_token: Valid access token
        """
        if not access_token:
            return

        try:
            userinfo = self.get_user_info(access_token)
            # Add metadata using utility function
            token = self.get_stored_token()
            token_expires_at = token.get("expires_at") if token else datetime.now(UTC).isoformat()
            enhanced_userinfo = add_userinfo_metadata(userinfo, token_expires_at)
            self.store_userinfo(enhanced_userinfo)
        except Exception:
            # If userinfo fetch fails, don't break the authentication flow
            pass

    def store_userinfo(self, userinfo):
        """Store user info securely using file storage."""
        try:
            self.token_storage.store_userinfo(userinfo)
        except TokenStorageError as e:
            raise AuthenticationError(f"Failed to store user info: {e}") from e

    def get_stored_userinfo(self):
        """Retrieve stored user info."""
        try:
            return self.token_storage.get_stored_userinfo()
        except TokenStorageError as e:
            raise AuthenticationError(f"Failed to retrieve user info: {e}") from e

    def is_userinfo_valid(self, userinfo):
        """Check if cached userinfo is still valid based on token expiry."""
        return is_userinfo_valid(userinfo)

    def get_cached_userinfo(self):
        """
        Get cached user info if valid, otherwise fetch fresh info.

        Returns:
            dict: User information or None if not available
        """
        # Try to get cached userinfo first
        cached_userinfo = self.get_stored_userinfo()
        if cached_userinfo and self.is_userinfo_valid(cached_userinfo):
            return cached_userinfo

        # If cached userinfo is invalid, try to fetch fresh info
        access_token = self.get_access_token()
        if access_token:
            try:
                self._fetch_and_cache_userinfo(access_token)
                return self.get_stored_userinfo()
            except Exception:
                pass

        return cached_userinfo  # Return cached even if expired, better than nothing

    def store_token(self, token):
        """Store token securely using file storage."""
        try:
            self.token_storage.store_token(token)
        except TokenStorageError as e:
            raise AuthenticationError(f"Failed to store token: {e}") from e

    def get_stored_token(self):
        """Retrieve stored token."""
        try:
            return self.token_storage.get_stored_token()
        except TokenStorageError as e:
            raise AuthenticationError(f"Failed to retrieve token: {e}") from e

    def clear_token(self):
        """Clear stored token and userinfo."""
        try:
            self.token_storage.clear_all()
        except TokenStorageError as e:
            raise AuthenticationError(f"Failed to clear tokens: {e}") from e

    def is_token_valid(self, token):
        """Check if token is still valid."""
        return is_token_valid(token)

    def is_authenticated(self):
        """Check if user is authenticated with a valid token."""
        token = self.get_stored_token()
        return token is not None and self.is_token_valid(token)

    def get_access_token(self):
        """
        Get a valid access token, refreshing if necessary.

        Returns:
            str: Valid access token or None
        """
        token = self.get_stored_token()
        if not token:
            return None

        # If token is valid, return it
        if self.is_token_valid(token):
            return extract_access_token(token)

        # Try to refresh if we have a refresh token
        refresh_token_str = extract_refresh_token(token)
        if refresh_token_str:
            try:
                refreshed = self.refresh_token(refresh_token_str)
                return extract_access_token(refreshed)
            except Exception:
                # Refresh failed, need to re-authenticate
                pass

        return None


class AuthenticationError(Exception):
    """Authentication-related errors."""

    pass
