import 'dart:typed_data';
import '../models/file_item.dart';
import '../models/viewer_session.dart';
import '../models/editor_session.dart';
import '../models/annotation.dart';
import 'api_client.dart';
import 'package:dio/dio.dart';
import 'package:archive/archive.dart';

class FileService {
  final ApiClient _apiClient;

  FileService(this._apiClient);

  String get baseUrl => _apiClient.baseUrl;

  Future<List<FileItem>> getFiles(String orgId, String path) async {
    if (path.isEmpty) {
      throw Exception('Path cannot be empty');
    }
    final pathParam = {'path': path};
    if (orgId.isEmpty) {
      return await _apiClient.getList(
        '/user/files',
        queryParameters: pathParam,
        fromJson: (data) => FileItem(
          id: data['id'],
          name: data['name'],
          path: data['path'],
          type: data['type'] == 'file' ? FileType.file : FileType.folder,
          size: data['size'],
          lastModified: DateTime.parse(data['lastModified']),
        ),
      );
    }

    return await _apiClient.getList(
      '/orgs/$orgId/files',
      queryParameters: pathParam,
      fromJson: (data) => FileItem(
        id: data['id'],
        name: data['name'],
        path: data['path'],
        type: data['type'] == 'file' ? FileType.file : FileType.folder,
        size: data['size'],
        lastModified: DateTime.parse(data['lastModified']),
      ),
    );
  }

  Future<FileItem?> getFile(String orgId, String path) async {
    throw UnimplementedError();
  }

  Future<void> uploadFile(String orgId, FileItem file) async {
    // If bytes or localPath available, send multipart upload with field 'file'
    final Map<String, dynamic> fields = {'path': file.path};
    FormData formData;

    if (file.bytes != null) {
      formData = FormData.fromMap({
        ...fields,
        'file': MultipartFile.fromBytes(file.bytes!, filename: file.name),
      });
    } else if (file.localPath != null) {
      formData = FormData.fromMap({
        ...fields,
        'file': MultipartFile.fromFile(file.localPath!, filename: file.name),
      });
    } else {
      // Fallback to metadata-only create (folders or client that can't send file content)
      final data = {
        'name': file.name,
        'path': file.path,
        'type': file.type == FileType.file ? 'file' : 'folder',
        'size': file.size,
      };
      if (orgId.isEmpty) {
        await _apiClient.post('/user/files', data: data, fromJson: (d) => null);
        return;
      }
      await _apiClient.post(
        '/orgs/$orgId/files',
        data: data,
        fromJson: (d) => null,
      );
      return;
    }

    final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files';
    await _apiClient.post(endpoint, data: formData, fromJson: (d) => null);
  }

  Future<void> deleteFile(String orgId, String path) async {
    final data = {'path': path};
    if (orgId.isEmpty) {
      await _apiClient.post(
        '/user/files/delete',
        data: data,
        fromJson: (d) => null,
      );
      return;
    }
    await _apiClient.post(
      '/orgs/$orgId/files/delete',
      data: data,
      fromJson: (d) => null,
    );
  }

  Future<String> getDownloadUrl(String orgId, String path) async {
    // Return the download URL for the file
    if (orgId.isEmpty) {
      return '/user/files/download?path=${Uri.encodeComponent(path)}';
    }
    return '/orgs/$orgId/files/download?path=${Uri.encodeComponent(path)}';
  }

  Future<String> getFileUrl({
    required String orgId,
    required String filePath,
    String? fileName,
  }) async {
    // Get authentication token
    final token = _apiClient.currentToken;
    if (token == null) {
      throw Exception('No authentication token available');
    }

    // Return the full download URL with token
    final path = orgId.isEmpty
        ? '/user/files/download?path=${Uri.encodeComponent(filePath)}&token=${Uri.encodeComponent(token)}'
        : '/orgs/$orgId/files/download?path=${Uri.encodeComponent(filePath)}&token=${Uri.encodeComponent(token)}';
    return '$baseUrl$path';
  }

  Future<void> createFolder(
    String orgId,
    String parentPath,
    String folderName,
  ) async {
    // Normalize folder name to avoid accidental leading slashes creating double-slash paths
    final normalizedName = folderName
        .replaceAll(RegExp(r'^/+'), '')
        .replaceAll(RegExp(r'/+$'), '');
    if (normalizedName.isEmpty) {
      throw Exception('Folder name cannot be empty');
    }

    // Construct proper path: /parent/folder or /folder for root
    String path;
    if (parentPath == '/') {
      path = '/$normalizedName';
    } else {
      // Ensure parentPath doesn't end with / before appending
      final cleanParent = parentPath.endsWith('/')
          ? parentPath.substring(0, parentPath.length - 1)
          : parentPath;
      path = '$cleanParent/$normalizedName';
    }

    final data = {
      'name': normalizedName,
      'path': path,
      'type': 'folder',
      'size': 0,
    };
    if (orgId.isEmpty) {
      await _apiClient.post('/user/files', data: data, fromJson: (d) => null);
      return;
    }
    await _apiClient.post(
      '/orgs/$orgId/files',
      data: data,
      fromJson: (d) => null,
    );
  }

  Future<void> moveFile(
    String orgId,
    String sourcePath,
    String targetPath,
  ) async {
    final endpoint = orgId.isEmpty
        ? '/user/files/move'
        : '/orgs/$orgId/files/move';
    await _apiClient.post(
      endpoint,
      data: {'sourcePath': sourcePath, 'targetPath': targetPath},
      fromJson: (d) => null,
    );
  }

  Future<void> renameFile(String orgId, String path, String newName) async {
    throw UnimplementedError();
  }

  Future<List<FileItem>> searchFiles(String orgId, String query) async {
    throw UnimplementedError();
  }

  Future<ViewerSession> requestViewerSession(
    String orgId,
    String fileId,
  ) async {
    if (fileId.isEmpty) {
      throw Exception('fileId cannot be empty');
    }
    final path = orgId.isEmpty
        ? '/user/files/$fileId/view'
        : '/orgs/$orgId/files/$fileId/view';
    return await _apiClient.get(
      path,
      fromJson: (data) => ViewerSession.fromJson(data),
    );
  }

  Future<EditorSession> requestEditorSession(
    String orgId,
    String fileId,
  ) async {
    if (fileId.isEmpty) {
      throw Exception('fileId cannot be empty');
    }
    final path = orgId.isEmpty
        ? '/user/files/$fileId/edit'
        : '/orgs/$orgId/files/$fileId/edit';
    return await _apiClient.get(
      path,
      fromJson: (data) => EditorSession.fromJson(data),
    );
  }

  Future<void> saveAnnotations(
    String orgId,
    String fileId,
    List<Annotation> annotations,
  ) async {
    if (fileId.isEmpty) {
      throw Exception('fileId cannot be empty');
    }
    final path = orgId.isEmpty
        ? '/user/files/$fileId/annotations'
        : '/orgs/$orgId/files/$fileId/annotations';
    await _apiClient.post(
      path,
      data: {
        'annotations': annotations.map((a) => a.toJson()).toList(),
        'baseVersionId': '1', // mock
      },
      fromJson: (data) => null,
    );
  }

  /// Creates an empty .docx document and returns the created file's ID
  Future<String> createDocument(
    String orgId,
    String parentPath,
    String fileName,
  ) async {
    // Ensure filename has .docx extension
    final docxName = fileName.endsWith('.docx') ? fileName : '$fileName.docx';

    // Generate minimal valid DOCX file (Office Open XML format)
    final bytes = _generateEmptyDocx();

    // Send parent directory as 'path' parameter
    final formData = FormData.fromMap({
      'path': parentPath,
      'file': MultipartFile.fromBytes(bytes, filename: docxName),
    });

    final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files';
    final response = await _apiClient.post(
      endpoint,
      data: formData,
      fromJson: (d) => d as Map<String, dynamic>,
    );

    // Return the file ID from the response
    return response['id'] as String;
  }

  /// Generates a minimal valid DOCX file (empty document)
  Uint8List _generateEmptyDocx() {
    final archive = Archive();

    // [Content_Types].xml - defines content types
    const contentTypes =
        '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>''';
    archive.addFile(
      ArchiveFile(
        '[Content_Types].xml',
        contentTypes.length,
        Uint8List.fromList(contentTypes.codeUnits),
      ),
    );

    // _rels/.rels - root relationships
    const rootRels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>''';
    archive.addFile(
      ArchiveFile(
        '_rels/.rels',
        rootRels.length,
        Uint8List.fromList(rootRels.codeUnits),
      ),
    );

    // word/document.xml - the actual document content (empty)
    const documentXml =
        '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="w14">
  <w:body>
    <w:p>
      <w:pPr>
        <w:pStyle w:val="Normal"/>
      </w:pPr>
    </w:p>
    <w:sectPr>
      <w:pgSz w:w="12240" w:h="15840"/>
      <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>
    </w:sectPr>
  </w:body>
</w:document>''';
    archive.addFile(
      ArchiveFile(
        'word/document.xml',
        documentXml.length,
        Uint8List.fromList(documentXml.codeUnits),
      ),
    );

    // word/styles.xml - document styles
    const stylesXml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="w14">
  <w:docDefaults>
    <w:rPrDefault>
      <w:rPr>
        <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri" w:eastAsia="Calibri"/>
        <w:sz w:val="22"/>
        <w:szCs w:val="22"/>
        <w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA"/>
      </w:rPr>
    </w:rPrDefault>
    <w:pPrDefault>
      <w:pPr>
        <w:spacing w:after="160" w:before="0" w:line="259" w:lineRule="auto"/>
      </w:pPr>
    </w:pPrDefault>
  </w:docDefaults>
  <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
    <w:name w:val="Normal"/>
    <w:qFormat/>
    <w:pPr>
      <w:spacing w:after="160" w:before="0" w:line="259" w:lineRule="auto"/>
    </w:pPr>
    <w:rPr>
      <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri" w:eastAsia="Calibri"/>
      <w:sz w:val="22"/>
      <w:szCs w:val="22"/>
      <w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA"/>
    </w:rPr>
  </w:style>
</w:styles>''';
    archive.addFile(
      ArchiveFile(
        'word/styles.xml',
        stylesXml.length,
        Uint8List.fromList(stylesXml.codeUnits),
      ),
    );

    // word/_rels/document.xml.rels - document relationships (empty but required)
    const docRels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>''';
    archive.addFile(
      ArchiveFile(
        'word/_rels/document.xml.rels',
        docRels.length,
        Uint8List.fromList(docRels.codeUnits),
      ),
    );

    // Encode as ZIP
    final zipEncoder = ZipEncoder();
    final zipData = zipEncoder.encode(archive);
    return Uint8List.fromList(zipData);
  }
}
