import fs from 'node:fs';
import * as fsPromises from 'node:fs/promises';
import path from 'node:path';

import * as plugins from './plugins.ts';
import {
  createInitialConfigFromEnv,
  normalizeConfig,
  type IAppConfig,
} from './config.ts';
import type { IFaxMessage } from './faxbox.ts';
import type { IFaxJob } from './faxjobs.ts';
import type { IVoicemailMessage } from './voicebox.ts';

interface ISiprouterDataStore {
  appConfig: IAppConfig;
  faxJobs: IFaxJob[];
  faxMessagesByBox: Record<string, IFaxMessage[]>;
  voicemailMessagesByBox: Record<string, IVoicemailMessage[]>;
}

type TLogFunction = (messageArg: string) => void;

const legacyConfigPath = path.join(process.cwd(), '.nogit', 'config.json');

function requiredEnv(keysArg: string[]): string {
  for (const key of keysArg) {
    const value = process.env[key];
    if (value) return value;
  }
  throw new Error(`Missing required environment variable: ${keysArg.join(' or ')}`);
}

function optionalNumber(valueArg: string | undefined, fallbackArg?: number): number | undefined {
  if (!valueArg) return fallbackArg;
  const parsed = Number(valueArg);
  return Number.isFinite(parsed) ? parsed : fallbackArg;
}

function optionalBoolean(valueArg: string | undefined, fallbackArg?: boolean): boolean | undefined {
  if (valueArg === undefined) return fallbackArg;
  return !['0', 'false', 'no', 'off'].includes(valueArg.toLowerCase());
}

function normalizeObjectKey(keyArg: string): string {
  const normalizedKey = keyArg.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
  if (normalizedKey.split('/').includes('..')) {
    throw new Error(`Invalid object key: ${keyArg}`);
  }
  return normalizedKey;
}

export class SiprouterStorage {
  private db!: InstanceType<typeof plugins.smartdata.SmartdataDb>;
  private store!: any;
  private bucket!: any;
  private readonly cacheDir = path.join(process.cwd(), '.nogit', 'cache');
  private readonly log: TLogFunction;

  constructor(logArg: TLogFunction) {
    this.log = logArg;
  }

  public async init(): Promise<void> {
    this.db = new plugins.smartdata.SmartdataDb(this.getMongoDescriptor() as any);
    await this.db.init();
    this.store = await this.db.createEasyStore('siprouter-data');

    const smartBucket = new plugins.smartbucket.SmartBucket(this.getS3Descriptor() as any);
    const bucketName = requiredEnv(['SIPROUTER_S3_BUCKET', 'S3_BUCKET']);
    this.bucket = await smartBucket.bucketExists(bucketName)
      ? await smartBucket.getBucketByName(bucketName)
      : await smartBucket.createBucket(bucketName);

    await fsPromises.mkdir(this.cacheDir, { recursive: true });
    this.log('[storage] smartdata and smartbucket initialized');
  }

  public async close(): Promise<void> {
    if (this.db) {
      await this.db.close();
    }
  }

  public async getAppConfig(): Promise<IAppConfig> {
    const storedConfig = await this.readKey('appConfig');
    if (storedConfig) {
      return normalizeConfig(storedConfig);
    }

    const legacyConfig = await this.readLegacyConfig();
    const initialConfig = legacyConfig || createInitialConfigFromEnv();
    await this.writeAppConfig(initialConfig);
    this.log(legacyConfig ? '[storage] imported legacy .nogit/config.json into smartdata' : '[storage] created initial smartdata config');
    return initialConfig;
  }

  public async writeAppConfig(configArg: IAppConfig): Promise<void> {
    await this.writeKey('appConfig', normalizeConfig(configArg));
  }

  public async getFaxJobs(): Promise<IFaxJob[]> {
    return (await this.readKey('faxJobs')) || [];
  }

  public async writeFaxJobs(jobsArg: IFaxJob[]): Promise<void> {
    await this.writeKey('faxJobs', jobsArg);
  }

  public async getVoicemailMessages(boxIdArg: string): Promise<IVoicemailMessage[]> {
    const allMessages = (await this.readKey('voicemailMessagesByBox')) || {};
    return allMessages[boxIdArg] || [];
  }

  public async writeVoicemailMessages(boxIdArg: string, messagesArg: IVoicemailMessage[]): Promise<void> {
    const allMessages = (await this.readKey('voicemailMessagesByBox')) || {};
    allMessages[boxIdArg] = messagesArg;
    await this.writeKey('voicemailMessagesByBox', allMessages);
  }

  public async getFaxMessages(boxIdArg: string): Promise<IFaxMessage[]> {
    const allMessages = (await this.readKey('faxMessagesByBox')) || {};
    return allMessages[boxIdArg] || [];
  }

  public async writeFaxMessages(boxIdArg: string, messagesArg: IFaxMessage[]): Promise<void> {
    const allMessages = (await this.readKey('faxMessagesByBox')) || {};
    allMessages[boxIdArg] = messagesArg;
    await this.writeKey('faxMessagesByBox', allMessages);
  }

  public async putFileObject(objectKeyArg: string, filePathArg: string): Promise<string> {
    const objectKey = normalizeObjectKey(objectKeyArg);
    const contents = await fsPromises.readFile(filePathArg);
    await this.bucket.fastPut({ path: objectKey, contents, overwrite: true });
    await this.removeCachedObject(objectKey);
    return objectKey;
  }

  public async putBufferObject(objectKeyArg: string, bufferArg: Buffer): Promise<string> {
    const objectKey = normalizeObjectKey(objectKeyArg);
    await this.bucket.fastPut({ path: objectKey, contents: bufferArg, overwrite: true });
    await this.removeCachedObject(objectKey);
    return objectKey;
  }

  public async getObjectAsCachedFile(objectKeyArg: string, fileNameArg?: string): Promise<string | null> {
    const objectKey = normalizeObjectKey(objectKeyArg);
    const cachePath = this.getCachePath(objectKey);
    try {
      if (fs.existsSync(cachePath)) {
        return cachePath;
      }
      const contents = await this.bucket.fastGet({ path: objectKey });
      await fsPromises.mkdir(path.dirname(cachePath), { recursive: true });
      await fsPromises.writeFile(cachePath, contents);
      return cachePath;
    } catch {
      if (fileNameArg) {
        const fallbackPath = path.join(this.cacheDir, path.basename(fileNameArg));
        return fs.existsSync(fallbackPath) ? fallbackPath : null;
      }
      return null;
    }
  }

  public async removeObject(objectKeyArg: string | undefined): Promise<void> {
    if (!objectKeyArg) return;
    const objectKey = normalizeObjectKey(objectKeyArg);
    try {
      await this.bucket.fastRemove({ path: objectKey });
    } catch {
      // Missing objects are harmless during metadata cleanup.
    }
    await this.removeCachedObject(objectKey);
  }

  private getCachePath(objectKeyArg: string): string {
    return path.join(this.cacheDir, normalizeObjectKey(objectKeyArg));
  }

  private async removeCachedObject(objectKeyArg: string): Promise<void> {
    await fsPromises.rm(this.getCachePath(objectKeyArg), { force: true }).catch(() => {});
  }

  private async readLegacyConfig(): Promise<IAppConfig | null> {
    try {
      const raw = await fsPromises.readFile(legacyConfigPath, 'utf8');
      return normalizeConfig(JSON.parse(raw) as IAppConfig);
    } catch {
      return null;
    }
  }

  private async readKey<TKey extends keyof ISiprouterDataStore>(keyArg: TKey): Promise<ISiprouterDataStore[TKey] | undefined> {
    try {
      return await this.store.readKey(keyArg) as ISiprouterDataStore[TKey] | undefined;
    } catch {
      return undefined;
    }
  }

  private async writeKey<TKey extends keyof ISiprouterDataStore>(
    keyArg: TKey,
    valueArg: ISiprouterDataStore[TKey],
  ): Promise<void> {
    await this.store.writeKey(keyArg, valueArg);
  }

  private getMongoDescriptor(): Record<string, string> {
    const mongoDbUrl = requiredEnv([
      'SIPROUTER_MONGODB_URL',
      'MONGODB_URI',
      'MONGODB_URL',
    ]);
    const descriptor: Record<string, string> = {
      mongoDbUrl,
      mongoDbName: process.env.SIPROUTER_MONGODB_NAME || process.env.MONGODB_DATABASE || process.env.MONGODB_NAME || 'siprouter',
    };

    const mongoDbUser = process.env.SIPROUTER_MONGODB_USER || process.env.MONGODB_USERNAME || process.env.MONGODB_USER;
    const mongoDbPass = process.env.SIPROUTER_MONGODB_PASS || process.env.MONGODB_PASSWORD || process.env.MONGODB_PASS;
    if (mongoDbUser) descriptor.mongoDbUser = mongoDbUser;
    if (mongoDbPass) descriptor.mongoDbPass = mongoDbPass;
    return descriptor;
  }

  private getS3Descriptor(): Record<string, string | number | boolean> {
    const rawEndpoint = requiredEnv(['SIPROUTER_S3_ENDPOINT', 'S3_ENDPOINT', 'AWS_ENDPOINT_URL']);
    let endpoint = rawEndpoint;
    let port = optionalNumber(process.env.SIPROUTER_S3_PORT || process.env.S3_PORT);
    let useSsl = optionalBoolean(process.env.SIPROUTER_S3_USESSL || process.env.S3_USESSL || process.env.S3_USE_SSL);

    if (/^https?:\/\//.test(rawEndpoint)) {
      const url = new URL(rawEndpoint);
      endpoint = url.hostname;
      port = url.port ? Number(url.port) : port;
      useSsl = url.protocol === 'https:';
    }

    return {
      endpoint,
      accessKey: requiredEnv(['SIPROUTER_S3_ACCESS_KEY', 'S3_ACCESS_KEY', 'AWS_ACCESS_KEY_ID']),
      accessSecret: requiredEnv(['SIPROUTER_S3_SECRET_KEY', 'S3_SECRET_KEY', 'AWS_SECRET_ACCESS_KEY']),
      region: process.env.SIPROUTER_S3_REGION || process.env.S3_REGION || process.env.AWS_REGION || 'us-east-1',
      ...(port ? { port } : {}),
      ...(useSsl !== undefined ? { useSsl } : {}),
    };
  }
}
