import { createHash, randomBytes } from "node:crypto";
import type { DataStore } from "../db/store";
import { issueToken } from "./auth";

const SETUP_SESSION_TTL_MS = 10 * 60 * 1000;

type SetupSession = {
    sessionToken: string;
    setupKeyHash: string;
    expiresAtMs: number;
};

export type SetupStatus = {
    setupLocked: boolean;
    setupCompletedAt: string | null;
    usedKeyCount: number;
    hasSetupKeys: boolean;
    setupAvailable: boolean;
};

export type SetupCompletionResult = {
    streamId: string;
    streamName: string;
    adminToken: string;
    viewerToken: string | null;
    adminUrl: string;
    overlayUrl: string | null;
    setupCompletedAt: string;
};

function hashSetupKey(value: string): string {
    return createHash("sha256").update(value, "utf8").digest("hex");
}

export class SetupWizardService {
    private readonly sessions = new Map<string, SetupSession>();
    private readonly configuredKeyHashes: Set<string>;

    public constructor(
        private readonly store: DataStore,
        setupKeys: string[],
        private readonly tokenPepper: string
    ) {
        this.configuredKeyHashes = new Set(
            setupKeys.map((key) => hashSetupKey(key.trim())).filter((hash) => hash.length > 0)
        );
    }

    public async getStatus(): Promise<SetupStatus> {
        const state = await this.store.getSetupState();
        const usedConfiguredKeyCount = state.usedSetupKeyHashes.filter((hash) =>
            this.configuredKeyHashes.has(hash)
        ).length;
        const exhausted = this.configuredKeyHashes.size > 0 && usedConfiguredKeyCount >= this.configuredKeyHashes.size;
        return {
            setupLocked: exhausted,
            setupCompletedAt: state.setupCompletedAt,
            usedKeyCount: usedConfiguredKeyCount,
            hasSetupKeys: this.configuredKeyHashes.size > 0,
            setupAvailable: !exhausted && this.configuredKeyHashes.size > 0
        };
    }

    public async authenticateSetupKey(rawSetupKey: string): Promise<string | null> {
        const setupKey = rawSetupKey.trim();
        if (!setupKey) {
            return null;
        }

        const status = await this.getStatus();
        if (status.setupLocked) {
            return null;
        }

        const setupKeyHash = hashSetupKey(setupKey);
        if (!this.configuredKeyHashes.has(setupKeyHash)) {
            return null;
        }
        const state = await this.store.getSetupState();
        if (state.usedSetupKeyHashes.includes(setupKeyHash)) {
            return null;
        }

        const sessionToken = `setup_${randomBytes(24).toString("base64url")}`;
        this.sessions.set(sessionToken, {
            sessionToken,
            setupKeyHash,
            expiresAtMs: Date.now() + SETUP_SESSION_TTL_MS
        });
        return sessionToken;
    }

    public async completeSetup(
        setupSessionToken: string,
        streamName: string,
        requestOrigin: string,
        includeOverlay: boolean
    ): Promise<SetupCompletionResult | null> {
        const session = this.getValidSession(setupSessionToken);
        if (!session) {
            return null;
        }

        const normalizedStreamName = streamName.trim();
        if (!normalizedStreamName) {
            return null;
        }

        const status = await this.getStatus();
        const state = await this.store.getSetupState();
        if (status.setupLocked || state.usedSetupKeyHashes.includes(session.setupKeyHash)) {
            this.sessions.delete(setupSessionToken);
            return null;
        }

        const stream = await this.store.createStream({ name: normalizedStreamName });
        const adminIssued = await issueToken(this.store, stream.id, "admin", this.tokenPepper);
        const viewerIssued = includeOverlay
            ? await issueToken(this.store, stream.id, "viewer", this.tokenPepper)
            : null;

        const setupCompletedAt = new Date().toISOString();
        await this.store.markSetupKeyUsed(session.setupKeyHash);
        await this.store.markSetupCompleted(setupCompletedAt);
        this.sessions.delete(setupSessionToken);

        return {
            streamId: stream.id,
            streamName: stream.name,
            adminToken: adminIssued.plainToken,
            viewerToken: viewerIssued?.plainToken ?? null,
            adminUrl: `${requestOrigin}/admin`,
            overlayUrl: viewerIssued
                ? `${requestOrigin}/overlay?token=${encodeURIComponent(viewerIssued.plainToken)}`
                : null,
            setupCompletedAt
        };
    }

    private getValidSession(setupSessionToken: string): SetupSession | null {
        const session = this.sessions.get(setupSessionToken);
        if (!session) {
            return null;
        }
        if (session.expiresAtMs < Date.now()) {
            this.sessions.delete(setupSessionToken);
            return null;
        }
        return session;
    }
}
