/**
 * Minimal structured logger + performance-metric emitter.
 *
 * Two responsibilities live here to keep the common layer thin:
 *
 *  1. **Leveled logging** — wraps `console.*` with a monotonically increasing
 *     severity threshold so we can downgrade chatty modules in production.
 *  2. **Performance埋点** — records named samples (e.g. touch→response latency)
 *     and computes p50/p95/avg for QA validation of requirement 20.1-20.6.
 *
 * Both halves are deliberately kept independent: the `metric()` API can be
 * routed to a BI endpoint later without touching the logging API.
 */

export enum LogLevel {
    Debug = 0,
    Info = 1,
    Warn = 2,
    Error = 3,
    Silent = 4,
}

export type LogSink = (level: LogLevel, module: string, msg: string, ...rest: unknown[]) => void;

export interface MetricSample {
    /** Metric name (e.g. 'touch_response_ms'). */
    name: string;
    /** Numeric value (ms, count, %, etc.). */
    value: number;
    /** Optional tags for slicing (e.g. `{ button: 'jump' }`). */
    tags?: Record<string, string | number>;
    /** `realTime` timestamp, ms since app start. */
    ts: number;
}

export interface MetricAggregate {
    count: number;
    min: number;
    max: number;
    avg: number;
    p50: number;
    p95: number;
}

export class Logger {
    private threshold: LogLevel = LogLevel.Debug;

    private sink: LogSink = (level, module, msg, ...rest) => {
        const prefix = `[${LogLevel[level]}][${module}]`;
        switch (level) {
            case LogLevel.Error:
                // eslint-disable-next-line no-console
                console.error(prefix, msg, ...rest);
                break;
            case LogLevel.Warn:
                // eslint-disable-next-line no-console
                console.warn(prefix, msg, ...rest);
                break;
            case LogLevel.Info:
                // eslint-disable-next-line no-console
                console.info(prefix, msg, ...rest);
                break;
            default:
                // eslint-disable-next-line no-console
                console.log(prefix, msg, ...rest);
        }
    };

    private readonly metrics = new Map<string, number[]>();
    private readonly startedTimers = new Map<string, number>();

    /** Control verbosity globally (call once at boot). */
    public setLevel(level: LogLevel): void {
        this.threshold = level;
    }

    /** Redirect logs (used in tests to assert against messages). */
    public setSink(sink: LogSink): void {
        this.sink = sink;
    }

    public debug(mod: string, msg: string, ...rest: unknown[]): void {
        this.dispatch(LogLevel.Debug, mod, msg, rest);
    }
    public info(mod: string, msg: string, ...rest: unknown[]): void {
        this.dispatch(LogLevel.Info, mod, msg, rest);
    }
    public warn(mod: string, msg: string, ...rest: unknown[]): void {
        this.dispatch(LogLevel.Warn, mod, msg, rest);
    }
    public error(mod: string, msg: string, ...rest: unknown[]): void {
        this.dispatch(LogLevel.Error, mod, msg, rest);
    }

    // ---------- metric API ----------

    /** Record a single metric sample. */
    public metric(sample: Omit<MetricSample, 'ts'>): void {
        const list = this.metrics.get(sample.name) ?? [];
        list.push(sample.value);
        this.metrics.set(sample.name, list);
    }

    /** Start a named stopwatch. */
    public timerStart(name: string, now: number = Logger.now()): void {
        this.startedTimers.set(name, now);
    }

    /**
     * Stop a named stopwatch and record its elapsed time (ms) under `name`.
     * Returns the elapsed value or `undefined` if the timer was not started.
     */
    public timerEnd(name: string, now: number = Logger.now()): number | undefined {
        const start = this.startedTimers.get(name);
        if (start === undefined) {
            return undefined;
        }
        this.startedTimers.delete(name);
        const elapsed = now - start;
        this.metric({ name, value: elapsed });
        return elapsed;
    }

    /** Compute aggregate stats (used by QA dashboards and test assertions). */
    public aggregate(name: string): MetricAggregate | undefined {
        const list = this.metrics.get(name);
        if (!list || list.length === 0) {
            return undefined;
        }
        const sorted = list.slice().sort((a, b) => a - b);
        const count = sorted.length;
        const sum = sorted.reduce((s, v) => s + v, 0);
        const pct = (p: number): number => {
            // Inclusive nearest-rank definition (matches common QA tools).
            const rank = Math.min(count - 1, Math.max(0, Math.ceil((p / 100) * count) - 1));
            return sorted[rank];
        };
        return {
            count,
            min: sorted[0],
            max: sorted[count - 1],
            avg: sum / count,
            p50: pct(50),
            p95: pct(95),
        };
    }

    /** Clear all recorded metrics (useful between unit tests). */
    public resetMetrics(): void {
        this.metrics.clear();
        this.startedTimers.clear();
    }

    private dispatch(level: LogLevel, mod: string, msg: string, rest: unknown[]): void {
        if (level < this.threshold) {
            return;
        }
        this.sink(level, mod, msg, ...rest);
    }

    private static now(): number {
        if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
            return performance.now();
        }
        return Date.now();
    }
}

/** Shared project-wide logger. */
export const globalLogger = new Logger();
