import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawDataSourceEntity, NetaClawDataSourceExtra } from '../entity/data_source.js';
import { MysqlPoolManager } from './mysql_pool.js';
import { SecretCryptoService } from './secret_crypto.js';

export interface NetaClawDataSourceAdminSafe {
  id: number;
  name: string;
  label: string | null;
  type: string;
  host: string;
  port: number;
  database: string;
  username: string;
  readonly: boolean;
  status: number;
  allowedAgentIds: number[] | null;
  extra: NetaClawDataSourceEntity['extra'];
  hasPassword: boolean;
}

export interface NetaClawDataSourceAgentSummary {
  name: string;
  label: string | null;
  database: string;
  status: number;
}

export interface SaveDataSourceConfigInput {
  id?: number;
  name?: string;
  label?: string | null;
  type?: string;
  host?: string;
  port?: number;
  database?: string;
  username?: string;
  readonly?: boolean;
  status?: number;
  allowedAgentIds?: number[] | null;
  extra?: NetaClawDataSourceEntity['extra'];
  password?: string;
}

export type NetaClawDataSourceSaveInput = Pick<
  SaveDataSourceConfigInput,
  | 'id'
  | 'name'
  | 'label'
  | 'host'
  | 'port'
  | 'database'
  | 'username'
  | 'status'
  | 'allowedAgentIds'
  | 'extra'
  | 'password'
>;

export interface NetaClawDataSourceTestConnectionResult {
  ok: boolean;
  error?: string;
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class NetaClawDataSourceService {
  @InjectEntityModel(NetaClawDataSourceEntity)
  dataSourceRepo: Repository<NetaClawDataSourceEntity>;

  @Inject()
  secretCrypto: SecretCryptoService;

  @Inject()
  mysqlPoolManager: MysqlPoolManager;

  toAdminSafe(entity: NetaClawDataSourceEntity): NetaClawDataSourceAdminSafe {
    return {
      id: entity.id,
      name: entity.name,
      label: entity.label,
      type: entity.type,
      host: entity.host,
      port: entity.port,
      database: entity.database,
      username: entity.username,
      readonly: entity.readonly,
      status: entity.status,
      allowedAgentIds: entity.allowedAgentIds,
      extra: this.toAdminSafeExtra(entity.extra),
      hasPassword: !!entity.passwordEncrypted,
    };
  }

  toAgentSummary(entity: NetaClawDataSourceEntity): NetaClawDataSourceAgentSummary {
    return {
      name: entity.name,
      label: entity.label,
      database: entity.database,
      status: entity.status,
    };
  }

  async listAdminSafe(): Promise<NetaClawDataSourceAdminSafe[]> {
    const list = await this.dataSourceRepo.find({ order: { id: 'DESC' } });
    return list.map(entity => this.toAdminSafe(entity));
  }

  async listForAgent(agentId: number): Promise<NetaClawDataSourceAgentSummary[]> {
    const list = await this.dataSourceRepo.find({
      where: { type: 'mysql', status: 1 },
      order: { id: 'DESC' },
    });

    return list
      .filter(entity =>
        entity.type === 'mysql' &&
        entity.status === 1 &&
        Array.isArray(entity.allowedAgentIds) &&
        entity.allowedAgentIds.includes(agentId)
      )
      .map(entity => this.toAgentSummary(entity));
  }

  async getAuthorizedSource(name: string, agentId: number): Promise<NetaClawDataSourceEntity> {
    const source = await this.dataSourceRepo.findOne({
      where: { name, type: 'mysql', status: 1 },
    });
    if (!source) {
      throw new Error('data_source_not_found');
    }
    if (!Array.isArray(source.allowedAgentIds) || !source.allowedAgentIds.includes(Number(agentId))) {
      throw new Error('data_source_not_authorized');
    }
    return source;
  }

  normalizeExtra(extra: NetaClawDataSourceExtra | null | undefined): NetaClawDataSourceExtra {
    return {
      allowedTables: this.normalizeStringList(extra?.allowedTables),
      blockedTables: this.normalizeStringList(extra?.blockedTables),
      maskedColumns: this.normalizeMaskedColumns(extra?.maskedColumns),
      schemaVisibility: extra?.schemaVisibility === 'all-names-only' ? 'all-names-only' : 'allowed-only',
      queryTimeoutMs: this.clampNumber(extra?.queryTimeoutMs, 10000, 1000, 30000),
      maxRows: this.clampNumber(extra?.maxRows, 200, 1, 1000),
      maxJoinTables: this.clampNumber(extra?.maxJoinTables, 4, 1, 6),
      poolConnectionLimit: this.clampNumber(extra?.poolConnectionLimit, 3, 1, 10),
      ...(extra?.ssl !== undefined ? { ssl: extra.ssl } : {}),
      connectTimeout: this.clampNumber(extra?.connectTimeout, 10000, 1000, 30000),
    };
  }

  async saveConfig(input: NetaClawDataSourceSaveInput): Promise<NetaClawDataSourceAdminSafe> {
    const existing = input.id
      ? await this.dataSourceRepo.findOne({ where: { id: input.id } })
      : null;

    const payload = this.compactDefined({
      id: existing?.id ?? input.id,
      name: input.name ?? existing?.name,
      label: input.label !== undefined ? input.label : existing?.label,
      host: input.host ?? existing?.host,
      port: input.port ?? existing?.port ?? 3306,
      database: input.database ?? existing?.database,
      username: input.username ?? existing?.username,
      type: 'mysql',
      readonly: true,
      status: input.status ?? existing?.status ?? 1,
      allowedAgentIds: this.resolveAllowedAgentIds(input.allowedAgentIds, existing),
      extra: this.normalizeExtra(this.resolveExtra(input.extra, existing)),
      passwordEncrypted: this.resolvePasswordEncrypted(input.password, existing),
    });
    this.assertRequiredConfig(payload);

    const entity = this.dataSourceRepo.create(payload);
    const saved = await this.dataSourceRepo.save(entity);
    if (saved.id) {
      await this.mysqlPoolManager.closePool(saved.id);
    }
    return this.toAdminSafe(saved);
  }

  async delete(id: number): Promise<void> {
    await this.dataSourceRepo.delete(id);
    await this.mysqlPoolManager.closePool(id);
  }

  async testConnection(input: NetaClawDataSourceSaveInput): Promise<NetaClawDataSourceTestConnectionResult> {
    const existing = input.id
      ? await this.dataSourceRepo.findOne({ where: { id: input.id } })
      : null;
    const source = Object.assign(new NetaClawDataSourceEntity(), {
      ...existing,
      ...this.compactDefined({
        id: input.id ?? existing?.id,
        name: input.name ?? existing?.name,
        label: input.label !== undefined ? input.label : existing?.label,
        host: input.host ?? existing?.host,
        port: input.port ?? existing?.port ?? 3306,
        database: input.database ?? existing?.database,
        username: input.username ?? existing?.username,
        status: input.status ?? existing?.status ?? 1,
        allowedAgentIds: this.resolveAllowedAgentIds(input.allowedAgentIds, existing),
        extra: this.normalizeExtra(this.resolveExtra(input.extra, existing)),
        type: 'mysql',
        readonly: true,
        passwordEncrypted: this.resolvePasswordEncrypted(input.password, existing),
      }),
    });
    this.assertRequiredConfig(source);
    let pool: Awaited<ReturnType<MysqlPoolManager['createTransientPool']>> | undefined;

    try {
      pool = await this.mysqlPoolManager.createTransientPool(source);
      await pool.query('SELECT 1 AS ok');
      return { ok: true };
    } catch (error) {
      return { ok: false, error: this.sanitizeMysqlConnectionError(error) };
    } finally {
      if (pool) {
        await pool.end();
      }
    }
  }

  private resolvePasswordEncrypted(
    password: string | undefined,
    existing: NetaClawDataSourceEntity | null
  ): string | null {
    if (password && password.trim()) {
      return this.secretCrypto.encryptText(password);
    }
    return existing?.passwordEncrypted ?? null;
  }

  private assertRequiredConfig(value: Partial<NetaClawDataSourceEntity>): void {
    for (const key of ['name', 'host', 'database', 'username'] as const) {
      const current = value[key];
      if (typeof current !== 'string' || current.trim().length === 0) {
        throw new Error(`data_source_${key}_required`);
      }
    }
  }

  private resolveExtra(
    inputExtra: NetaClawDataSourceExtra | null | undefined,
    existing: NetaClawDataSourceEntity | null
  ): NetaClawDataSourceExtra | null | undefined {
    if (inputExtra === undefined) {
      return existing?.extra;
    }
    return {
      ...(existing?.extra ?? {}),
      ...(inputExtra ?? {}),
    };
  }

  private resolveAllowedAgentIds(
    inputAllowedAgentIds: number[] | null | undefined,
    existing: NetaClawDataSourceEntity | null
  ): number[] {
    if (Array.isArray(inputAllowedAgentIds)) {
      return this.normalizeNumberList(inputAllowedAgentIds);
    }
    return this.normalizeNumberList(existing?.allowedAgentIds ?? []);
  }

  private toAdminSafeExtra(extra: NetaClawDataSourceExtra | null): NetaClawDataSourceExtra | null {
    if (!extra) {
      return extra;
    }
    if (extra.ssl && typeof extra.ssl === 'object') {
      return {
        ...extra,
        ssl: { enabled: true },
      };
    }
    return extra;
  }

  private normalizeStringList(value: unknown): string[] {
    if (!Array.isArray(value)) {
      return [];
    }
    const normalized = value
      .filter((item): item is string => typeof item === 'string')
      .map(item => item.trim().toLowerCase())
      .filter(Boolean);
    return [...new Set(normalized)];
  }

  private normalizeNumberList(value: unknown): number[] {
    if (!Array.isArray(value)) {
      return [];
    }
    const normalized = value
      .map(item => Number(item))
      .filter(item => Number.isFinite(item))
      .map(item => Math.trunc(item));
    return [...new Set(normalized)];
  }

  private normalizeMaskedColumns(value: unknown): NetaClawDataSourceExtra['maskedColumns'] {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return {};
    }

    const allowedModes = new Set(['hash', 'partial', 'redact']);
    const normalized: NonNullable<NetaClawDataSourceExtra['maskedColumns']> = {};
    for (const [rawKey, rawMode] of Object.entries(value)) {
      const key = rawKey.trim().toLowerCase();
      if (!/^[^.]+\.[^.]+$/.test(key) || !allowedModes.has(String(rawMode))) {
        continue;
      }
      normalized[key] = rawMode as 'hash' | 'partial' | 'redact';
    }

    return normalized;
  }

  private sanitizeMysqlConnectionError(error: unknown): string {
    const code = typeof error === 'object' && error && 'code' in error
      ? String((error as { code?: unknown }).code)
      : '';
    return code ? `mysql_connection_failed:${code}` : 'mysql_connection_failed';
  }

  private compactDefined<T extends Record<string, unknown>>(value: T): Partial<T> {
    return Object.fromEntries(
      Object.entries(value).filter(([, fieldValue]) => fieldValue !== undefined)
    ) as Partial<T>;
  }

  private clampNumber(value: number | undefined, fallback: number, min: number, max: number): number {
    if (!Number.isFinite(value)) {
      return fallback;
    }
    return Math.min(max, Math.max(min, Math.trunc(value as number)));
  }
}
