import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import sequelize from '../database.js';
import Club from '../models/Club.js';
import ClubDocument from '../models/ClubDocument.js';
import ClubDocumentVersion from '../models/ClubDocumentVersion.js';
import ClubDocumentLink from '../models/ClubDocumentLink.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const DOCUMENT_TYPES = new Set(['satzung', 'protokoll', 'nachweis', 'formular', 'vertrag', 'rechnung', 'other']);
const DOCUMENT_STATUSES = new Set(['active', 'draft', 'archived', 'obsolete']);
const VISIBILITY_SCOPES = new Set(['board', 'finance', 'trainers', 'all']);

function trimText(value, maxLength = null) {
  const normalized = String(value || '').trim();
  if (!normalized) return null;
  return maxLength ? normalized.slice(0, maxLength) : normalized;
}

function normalizePayload(payload = {}) {
  return {
    documentType: DOCUMENT_TYPES.has(payload.documentType) ? payload.documentType : 'other',
    title: trimText(payload.title, 255),
    description: trimText(payload.description),
    status: DOCUMENT_STATUSES.has(payload.status) ? payload.status : 'active',
    visibilityScope: VISIBILITY_SCOPES.has(payload.visibilityScope) ? payload.visibilityScope : 'board',
    changeNote: trimText(payload.changeNote),
    linkedEntityType: trimText(payload.linkedEntityType, 32),
    linkedEntityId: payload.linkedEntityId ? Number(payload.linkedEntityId) : null,
  };
}

function normalizeFileName(value = '') {
  return String(value || 'document').replace(/[^a-zA-Z0-9._-]+/g, '_');
}

function getStorageDir(clubId) {
  return path.join(__dirname, '..', 'uploads', 'club-documents', String(clubId));
}

function ensureStorageDir(clubId) {
  const uploadDir = getStorageDir(clubId);
  fs.mkdirSync(uploadDir, { recursive: true });
  return uploadDir;
}

function checksumFile(filePath) {
  const hash = crypto.createHash('sha256');
  const fileBuffer = fs.readFileSync(filePath);
  hash.update(fileBuffer);
  return hash.digest('hex');
}

function moveUploadedFile(tempPath, destinationPath) {
  try {
    fs.renameSync(tempPath, destinationPath);
  } catch (error) {
    if (error.code === 'EXDEV') {
      fs.copyFileSync(tempPath, destinationPath);
      fs.unlinkSync(tempPath);
      return;
    }
    throw error;
  }
}

function buildDocumentSnapshot(document, versions = [], links = []) {
  const orderedVersions = [...versions].sort((a, b) => Number(b.versionNo || 0) - Number(a.versionNo || 0));
  const latestVersion = orderedVersions[0] || null;
  return {
    ...document.get({ plain: true }),
    versionCount: versions.length,
    latestVersion: latestVersion ? latestVersion.get({ plain: true }) : null,
    versions: orderedVersions.map((version) => version.get({ plain: true })),
    links: links.map((link) => link.get({ plain: true })),
    linkedEntityCount: links.length,
  };
}

class ClubDocumentService {
  async listClubDocuments(clubId, filters = {}) {
    const [club, documents] = await Promise.all([
      Club.findByPk(clubId, { attributes: ['id'] }),
      ClubDocument.findAll({
        where: { clubId },
        include: [
          { model: ClubDocumentVersion, as: 'versions', required: false },
          { model: ClubDocumentLink, as: 'links', required: false },
        ],
        order: [['updatedAt', 'DESC'], ['createdAt', 'DESC']],
      }),
    ]);

    if (!club) {
      const error = new Error('Verein wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const search = trimText(filters.search)?.toLowerCase() || '';
    const typeFilter = trimText(filters.documentType);
    const statusFilter = trimText(filters.status);
    const visibilityFilter = trimText(filters.visibilityScope);

    return documents
      .filter((document) => {
        if (typeFilter && document.documentType !== typeFilter) return false;
        if (statusFilter && document.status !== statusFilter) return false;
        if (visibilityFilter && document.visibilityScope !== visibilityFilter) return false;
        if (!search) return true;

        const haystack = [
          document.title,
          document.description,
          document.documentType,
          document.status,
          document.visibilityScope,
          ...(document.versions || []).map((version) => version.fileName),
          ...(document.links || []).map((link) => `${link.linkedEntityType} ${link.linkedEntityId}`),
        ].filter(Boolean).join(' ').toLowerCase();

        return haystack.includes(search);
      })
      .map((document) => buildDocumentSnapshot(document, document.versions || [], document.links || []));
  }

  async createClubDocument(clubId, userId, payload = {}, file = null) {
    const normalized = normalizePayload(payload);
    if (!normalized.title) {
      const error = new Error('Bitte einen Dokumenttitel angeben.');
      error.status = 400;
      throw error;
    }
    if (!file) {
      const error = new Error('Eine Datei ist für das neue Dokument erforderlich.');
      error.status = 400;
      throw error;
    }

    const club = await Club.findByPk(clubId, { attributes: ['id'] });
    if (!club) {
      const error = new Error('Verein wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const uploadDir = ensureStorageDir(clubId);
    const safeFileName = normalizeFileName(file.originalname);
    const storageFileName = `${Date.now()}_${safeFileName}`;
    const storagePath = path.join(uploadDir, storageFileName);
    moveUploadedFile(file.path, storagePath);

    const checksum = checksumFile(storagePath);
    const transaction = await sequelize.transaction();
    try {
      const document = await ClubDocument.create({
        clubId,
        documentType: normalized.documentType,
        title: normalized.title,
        description: normalized.description,
        status: normalized.status,
        visibilityScope: normalized.visibilityScope,
        ownerUserId: userId || null,
        currentVersionNo: 1,
      }, { transaction });

      const version = await ClubDocumentVersion.create({
        documentId: document.id,
        versionNo: 1,
        fileName: safeFileName,
        storagePath,
        mimeType: file.mimetype,
        fileSizeBytes: file.size,
        checksumSha256: checksum,
        uploadedByUserId: userId || null,
        changeNote: normalized.changeNote,
      }, { transaction });

      if (normalized.linkedEntityType && normalized.linkedEntityId) {
        await ClubDocumentLink.create({
          documentId: document.id,
          linkedEntityType: normalized.linkedEntityType,
          linkedEntityId: normalized.linkedEntityId,
        }, { transaction });
      }

      await transaction.commit();
      return this.getClubDocumentById(clubId, document.id);
    } catch (error) {
      await transaction.rollback();
      if (fs.existsSync(storagePath)) {
        fs.unlinkSync(storagePath);
      }
      throw error;
    }
  }

  async updateClubDocument(clubId, documentId, userId, payload = {}, file = null) {
    const normalized = normalizePayload(payload);
    const document = await ClubDocument.findOne({
      where: { id: documentId, clubId },
      include: [
        { model: ClubDocumentVersion, as: 'versions', required: false },
        { model: ClubDocumentLink, as: 'links', required: false },
      ],
    });
    if (!document) {
      const error = new Error('Dokument wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const transaction = await sequelize.transaction();
    let uploadedStoragePath = null;
    try {
      await document.update({
        title: normalized.title || document.title,
        description: normalized.description,
        status: normalized.status,
        visibilityScope: normalized.visibilityScope,
        documentType: normalized.documentType,
      }, { transaction });

      if (file) {
        const uploadDir = ensureStorageDir(clubId);
        const nextVersionNo = Number(document.currentVersionNo || 1) + 1;
        const safeFileName = normalizeFileName(file.originalname);
        const storageFileName = `${Date.now()}_v${nextVersionNo}_${safeFileName}`;
        const storagePath = path.join(uploadDir, storageFileName);
        moveUploadedFile(file.path, storagePath);
        uploadedStoragePath = storagePath;
        const checksum = checksumFile(storagePath);

        await ClubDocumentVersion.create({
          documentId: document.id,
          versionNo: nextVersionNo,
          fileName: safeFileName,
          storagePath,
          mimeType: file.mimetype,
          fileSizeBytes: file.size,
          checksumSha256: checksum,
          uploadedByUserId: userId || null,
          changeNote: normalized.changeNote,
        }, { transaction });

        await document.update({
          currentVersionNo: nextVersionNo,
          archivedAt: normalized.status === 'archived' ? new Date() : null,
          status: normalized.status,
        }, { transaction });
      }

      if (normalized.linkedEntityType && normalized.linkedEntityId) {
        await ClubDocumentLink.findOrCreate({
          where: {
            documentId: document.id,
            linkedEntityType: normalized.linkedEntityType,
            linkedEntityId: normalized.linkedEntityId,
          },
          defaults: {
            documentId: document.id,
            linkedEntityType: normalized.linkedEntityType,
            linkedEntityId: normalized.linkedEntityId,
          },
          transaction,
        });
      }

      await transaction.commit();
      return this.getClubDocumentById(clubId, document.id);
    } catch (error) {
      await transaction.rollback();
      if (uploadedStoragePath && fs.existsSync(uploadedStoragePath)) {
        fs.unlinkSync(uploadedStoragePath);
      }
      throw error;
    }
  }

  async archiveClubDocument(clubId, documentId) {
    const document = await ClubDocument.findOne({ where: { id: documentId, clubId } });
    if (!document) {
      const error = new Error('Dokument wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await document.update({
      status: 'archived',
      archivedAt: new Date(),
    });
    return this.getClubDocumentById(clubId, document.id);
  }

  async deleteClubDocument(clubId, documentId) {
    const document = await ClubDocument.findOne({
      where: { id: documentId, clubId },
      include: [{ model: ClubDocumentVersion, as: 'versions', required: false }],
    });
    if (!document) {
      const error = new Error('Dokument wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    for (const version of document.versions || []) {
      if (version.storagePath && fs.existsSync(version.storagePath)) {
        fs.unlinkSync(version.storagePath);
      }
    }
    await ClubDocumentLink.destroy({ where: { documentId } });
    await ClubDocumentVersion.destroy({ where: { documentId } });
    await document.destroy();
    return true;
  }

  async getClubDocumentById(clubId, documentId) {
    const document = await ClubDocument.findOne({
      where: { id: documentId, clubId },
      include: [
        { model: ClubDocumentVersion, as: 'versions', required: false },
        { model: ClubDocumentLink, as: 'links', required: false },
      ],
    });
    if (!document) {
      return null;
    }
    return buildDocumentSnapshot(document, document.versions || [], document.links || []);
  }

  async getDocumentDownload(clubId, documentId, versionNo = null) {
    const document = await ClubDocument.findOne({
      where: { id: documentId, clubId },
      include: [{ model: ClubDocumentVersion, as: 'versions', required: false }],
    });
    if (!document) {
      const error = new Error('Dokument wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const versions = document.versions || [];
    const selectedVersion = versionNo
      ? versions.find((version) => Number(version.versionNo) === Number(versionNo))
      : [...versions].sort((a, b) => Number(b.versionNo || 0) - Number(a.versionNo || 0))[0];

    if (!selectedVersion) {
      const error = new Error('Dokumentversion wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    return { document: document.get({ plain: true }), version: selectedVersion.get({ plain: true }) };
  }
}

export default new ClubDocumentService();
