import {
    existsSync,
    mkdirSync,
    readFileSync,
    readdirSync,
    renameSync,
    unlinkSync,
    writeFileSync
} from "node:fs";
import { dirname, join } from "node:path";
import { randomUUID } from "node:crypto";
import type {
    DataStore,
    NewChatMessage,
    NewPoll,
    NewStream,
    NewToken,
    PollPatch
} from "./store";
import type { AppData, ChatMessage, Poll, Stream, Token } from "./types";

const EMPTY_DATA: AppData = {
    streams: [],
    tokens: [],
    chat_messages: [],
    polls: [],
    setupCompletedAt: null,
    usedSetupKeyHashes: [],
    setupLocked: false
};

function nowIso(): string {
    return new Date().toISOString();
}

function dateKeyFromIso(isoTimestamp: string): string {
    return isoTimestamp.slice(0, 10);
}

function normalizePoll(input: Partial<Poll>): Poll {
    const timestamp = nowIso();
    const isOpen = input.isOpen ?? true;
    return {
        id: input.id ?? randomUUID(),
        streamId: input.streamId ?? "",
        question: input.question ?? "",
        isOpen,
        votesYes: Number(input.votesYes ?? 0),
        votesNo: Number(input.votesNo ?? 0),
        createdAt: input.createdAt ?? timestamp,
        updatedAt: input.updatedAt ?? input.createdAt ?? timestamp,
        closedAt: input.closedAt ?? (isOpen ? null : timestamp)
    };
}

export type FileDataStoreOptions = {
    chatLogDir?: string;
    chatRetentionDays?: number;
};

export class FileDataStore implements DataStore {
    private readonly filePath: string;
    private readonly chatLogDir: string;
    private readonly chatRetentionDays: number;
    private data: AppData = { ...EMPTY_DATA };
    private lastRetentionDateKey: string | null = null;

    public constructor(filePath: string, options: FileDataStoreOptions = {}) {
        this.filePath = filePath;
        this.chatLogDir = options.chatLogDir ?? join(dirname(filePath), "chat");
        this.chatRetentionDays = Math.max(1, Math.floor(options.chatRetentionDays ?? 30));
    }

    public async init(): Promise<void> {
        mkdirSync(dirname(this.filePath), { recursive: true });
        mkdirSync(this.chatLogDir, { recursive: true });

        if (!existsSync(this.filePath)) {
            this.data = { ...EMPTY_DATA };
            this.persist();
            return;
        }

        try {
            const raw = readFileSync(this.filePath, "utf8");
            const parsed = JSON.parse(raw) as Partial<AppData>;

            this.data = {
                streams: parsed.streams ?? [],
                tokens: parsed.tokens ?? [],
                chat_messages: parsed.chat_messages ?? [],
                polls: (parsed.polls ?? []).map((poll) => normalizePoll(poll)),
                setupCompletedAt:
                    typeof parsed.setupCompletedAt === "string" ? parsed.setupCompletedAt : null,
                usedSetupKeyHashes: Array.isArray(parsed.usedSetupKeyHashes)
                    ? parsed.usedSetupKeyHashes.filter(
                          (value): value is string => typeof value === "string" && value.length > 0
                      )
                    : [],
                setupLocked: parsed.setupLocked === true
            };
        } catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to initialize datastore from ${this.filePath}: ${message}`);
        }

        this.pruneExpiredChatLogs(nowIso());
    }

    public async createStream(input: NewStream): Promise<Stream> {
        const stream: Stream = {
            id: randomUUID(),
            name: input.name,
            createdAt: nowIso()
        };

        this.data.streams.push(stream);
        this.persist();
        return stream;
    }

    public async listStreams(): Promise<Stream[]> {
        return this.data.streams.map((stream) => ({ ...stream }));
    }

    public async getStreamById(streamId: string): Promise<Stream | null> {
        const stream = this.data.streams.find((item) => item.id === streamId);
        return stream ?? null;
    }

    public async createToken(input: NewToken): Promise<Token> {
        const token: Token = {
            id: randomUUID(),
            streamId: input.streamId,
            role: input.role,
            tokenHash: input.tokenHash,
            createdAt: nowIso(),
            revokedAt: null
        };

        this.data.tokens.push(token);
        this.persist();
        return token;
    }

    public async getTokenByHash(tokenHash: string): Promise<Token | null> {
        const token = this.data.tokens.find(
            (item) => item.tokenHash === tokenHash && item.revokedAt === null
        );

        return token ?? null;
    }

    public async listTokensByStream(streamId: string): Promise<Token[]> {
        return this.data.tokens
            .filter((item) => item.streamId === streamId)
            .map((item) => ({ ...item }));
    }

    public async revokeTokenById(tokenId: string): Promise<boolean> {
        const token = this.data.tokens.find((item) => item.id === tokenId);
        if (!token || token.revokedAt !== null) {
            return false;
        }

        token.revokedAt = nowIso();
        this.persist();
        return true;
    }

    public async createChatMessage(input: NewChatMessage): Promise<ChatMessage> {
        const createdAt = nowIso();
        const record: ChatMessage = {
            id: randomUUID(),
            streamId: input.streamId,
            platform: input.platform,
            author: input.author,
            message: input.message,
            createdAt
        };

        this.appendChatLog(record);
        this.pruneExpiredChatLogs(createdAt);
        return { ...record };
    }

    public async listChatMessagesByStream(streamId: string): Promise<ChatMessage[]> {
        const dailyMessages = this.readAllChatLogs()
            .filter((item) => item.streamId === streamId)
            .map((item) => ({ ...item }));
        const legacyMessages = this.data.chat_messages
            .filter((item) => item.streamId === streamId)
            .map((item) => ({ ...item }));

        return [...legacyMessages, ...dailyMessages].sort((left, right) =>
            left.createdAt.localeCompare(right.createdAt)
        );
    }

    public async createPoll(input: NewPoll): Promise<Poll> {
        const timestamp = nowIso();
        const poll: Poll = {
            id: randomUUID(),
            streamId: input.streamId,
            question: input.question,
            isOpen: input.isOpen,
            votesYes: input.votesYes,
            votesNo: input.votesNo,
            createdAt: timestamp,
            updatedAt: timestamp,
            closedAt: input.isOpen ? null : timestamp
        };

        this.data.polls.push(poll);
        this.persist();
        return { ...poll };
    }

    public async updatePoll(pollId: string, patch: PollPatch): Promise<Poll | null> {
        const poll = this.data.polls.find((item) => item.id === pollId);
        if (!poll) {
            return null;
        }

        if (patch.question !== undefined) {
            poll.question = patch.question;
        }
        if (patch.votesYes !== undefined) {
            poll.votesYes = patch.votesYes;
        }
        if (patch.votesNo !== undefined) {
            poll.votesNo = patch.votesNo;
        }
        if (patch.isOpen !== undefined) {
            poll.isOpen = patch.isOpen;
            if (!patch.isOpen && poll.closedAt === null) {
                poll.closedAt = nowIso();
            }
        }
        poll.updatedAt = nowIso();
        this.persist();
        return { ...poll };
    }

    public async listPollsByStream(streamId: string): Promise<Poll[]> {
        return this.data.polls
            .filter((item) => item.streamId === streamId)
            .map((item) => ({ ...item }));
    }

    public async getSetupState(): Promise<{
        setupCompletedAt: string | null;
        usedSetupKeyHashes: string[];
        setupLocked: boolean;
    }> {
        return {
            setupCompletedAt: this.data.setupCompletedAt,
            usedSetupKeyHashes: [...this.data.usedSetupKeyHashes],
            setupLocked: this.data.setupLocked
        };
    }

    public async markSetupKeyUsed(setupKeyHash: string): Promise<void> {
        if (!this.data.usedSetupKeyHashes.includes(setupKeyHash)) {
            this.data.usedSetupKeyHashes.push(setupKeyHash);
            this.persist();
        }
    }

    public async markSetupCompleted(completedAt: string): Promise<void> {
        this.data.setupCompletedAt = completedAt;
        this.persist();
    }

    public async hasActiveAdminToken(): Promise<boolean> {
        return this.data.tokens.some((token) => token.role === "admin" && token.revokedAt === null);
    }

    private persist(): void {
        writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), "utf8");
    }

    private appendChatLog(record: ChatMessage): void {
        const dateKey = dateKeyFromIso(record.createdAt);
        const filePath = this.chatLogFilePath(dateKey);
        const messages = this.readChatLogFile(filePath);
        messages.push(record);
        this.writeJsonFile(filePath, messages);
    }

    private readAllChatLogs(): ChatMessage[] {
        if (!existsSync(this.chatLogDir)) {
            return [];
        }

        return readdirSync(this.chatLogDir)
            .filter((item) => /^\d{4}-\d{2}-\d{2}\.json$/.test(item))
            .sort()
            .flatMap((item) => this.readChatLogFile(join(this.chatLogDir, item)));
    }

    private readChatLogFile(filePath: string): ChatMessage[] {
        if (!existsSync(filePath)) {
            return [];
        }

        const parsed = JSON.parse(readFileSync(filePath, "utf8")) as unknown;
        if (!Array.isArray(parsed)) {
            return [];
        }

        return parsed.flatMap((item): ChatMessage[] => {
            if (!item || typeof item !== "object") {
                return [];
            }
            const candidate = item as Partial<ChatMessage>;
            if (
                typeof candidate.id !== "string" ||
                typeof candidate.streamId !== "string" ||
                typeof candidate.platform !== "string" ||
                typeof candidate.author !== "string" ||
                typeof candidate.message !== "string" ||
                typeof candidate.createdAt !== "string"
            ) {
                return [];
            }
            return [
                {
                    id: candidate.id,
                    streamId: candidate.streamId,
                    platform: candidate.platform,
                    author: candidate.author,
                    message: candidate.message,
                    createdAt: candidate.createdAt
                }
            ];
        });
    }

    private pruneExpiredChatLogs(timestampIso: string): void {
        const currentDateKey = dateKeyFromIso(timestampIso);
        if (this.lastRetentionDateKey === currentDateKey) {
            return;
        }
        this.lastRetentionDateKey = currentDateKey;

        if (!existsSync(this.chatLogDir)) {
            return;
        }

        const oldestAllowedMs =
            Date.parse(`${currentDateKey}T00:00:00.000Z`) -
            (this.chatRetentionDays - 1) * 24 * 60 * 60 * 1000;

        for (const fileName of readdirSync(this.chatLogDir)) {
            const match = /^(\d{4}-\d{2}-\d{2})\.json$/.exec(fileName);
            if (!match) {
                continue;
            }
            const fileDateMs = Date.parse(`${match[1]}T00:00:00.000Z`);
            if (Number.isFinite(fileDateMs) && fileDateMs < oldestAllowedMs) {
                unlinkSync(join(this.chatLogDir, fileName));
            }
        }
    }

    private chatLogFilePath(dateKey: string): string {
        return join(this.chatLogDir, `${dateKey}.json`);
    }

    private writeJsonFile(filePath: string, payload: unknown): void {
        mkdirSync(dirname(filePath), { recursive: true });
        const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
        writeFileSync(tempPath, JSON.stringify(payload, null, 2), "utf8");
        renameSync(tempPath, filePath);
    }
}
