/*
 * Silex website builder, free/libre no-code tool for makers.
 * Copyright (c) 2023 lexoyo and Silex Labs foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import fs from 'fs/promises'
import { createWriteStream } from 'fs'
import { ConnectorFile, StorageConnector, StatusCallback, ConnectorSession, toConnectorData, ConnectorFileContent} from './connectors'
import { dirname, join } from 'path'
import { ConnectorUser, WebsiteMeta, JobStatus, WebsiteId, ConnectorType, WebsiteMetaFileContent, WebsiteData, defaultWebsiteData, ConnectorOptions } from '../../types'
import { userInfo } from 'os'
import { requiredParam } from '../utils/validation'
import { ServerConfig } from '../config'
import { DEFAULT_WEBSITE_ID, WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
import { Readable } from 'stream'
import { v4 as uuid } from 'uuid'
import { fileURLToPath } from 'url'

// Variables needed for jest tests
if(!globalThis.__dirname) {
  // @ts-ignore
  globalThis.__dirname = dirname(process.cwd() + '/src/ts/server/connectors/FsStorage.ts')
  console.info('Redefining __dirname', globalThis.__dirname)
}

// Copy a folder recursively
async function copyDir(src, dest) {
  await fs.mkdir(dest, { recursive: true })
  const entries = await fs.readdir(src, { withFileTypes: true })

  for (const entry of entries) {
    const srcPath = join(src, entry.name)
    const destPath = join(dest, entry.name)

    entry.isDirectory() ?
      await copyDir(srcPath, destPath) :
      await fs.copyFile(srcPath, destPath)
  }
}


type FsSession = ConnectorSession

const USER_ICON = 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' height=\'1em\' viewBox=\'0 0 448 512\'%3E%3Cpath d=\'M304 128a80 80 0 1 0 -160 0 80 80 0 1 0 160 0zM96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM49.3 464H398.7c-8.9-63.3-63.3-112-129-112H178.3c-65.7 0-120.1 48.7-129 112zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3z\'/%3E%3C/svg%3E'
const FILE_ICON = '/assets/laptop.png'

interface FsOptions {
  path: string
  assetsFolder: string
}

export class FsStorage implements StorageConnector<FsSession> {
  connectorId = 'fs-storage'
  displayName = 'File system storage'
  icon = FILE_ICON
  disableLogout = true
  options: FsOptions
  connectorType = ConnectorType.STORAGE
  color = '#ffffff'
  background = '#006400'

  constructor(config: ServerConfig | null, opts: Partial<FsOptions>) {
    this.options = {
      path: join(__dirname, '..', '..', '..', '..', 'data'),
      assetsFolder: '/assets',
      ...opts,
    }
    this.initFs()
  }

  protected async initFs() {
    const stat = await fs.stat(this.options.path).catch(() => null)
    if (!stat) {
      // create data folder with a default website
      const id = DEFAULT_WEBSITE_ID
      await fs.mkdir(join(this.options.path, id, this.options.assetsFolder), { recursive: true })
      await this.setWebsiteMeta({}, id, { name: 'Default website', connectorUserSettings: {} })
      await this.updateWebsite({}, id, defaultWebsiteData)
      console.info(`> [FsStorage] Created ${id} in ${this.options.path}`)
    }
  }

  // ********************
  // Job utils methods
  // ********************
  private updateStatus(filesStatuses, status, statusCbk) {
    statusCbk && statusCbk({
      message: `<p>Writing files:<ul><li>${filesStatuses.map(({file, message}) => `${file.path}: ${message}`).join('</li><li>')}</li></ul></p>`,
      status,
    })
  }

  private initStatus(files) {
    return files.map(file => ({
      file,
      message: 'Waiting',
      status: JobStatus.IN_PROGRESS,
    }))
  }

  // ********************
  // Connector interface
  // ********************
  getOptions(formData: object): ConnectorOptions {
    return {}
  }

  async getOAuthUrl(session: FsSession): Promise<null> { return null }

  async getLoginForm(session: FsSession, redirectTo: string): Promise<string | null> {
    return null
  }
  async getSettingsForm(session: FsSession, redirectTo: string): Promise<string | null> {
    return null
  }

  async isLoggedIn(session: FsSession): Promise<boolean> {
    return true
  }

  async setToken(session: FsSession, query: object): Promise<void> {}

  async logout(session: FsSession): Promise<void> {}

  async getUser(session: FsSession): Promise<ConnectorUser> {
    const { username,  } = userInfo()
    return {
      name: username,
      picture: USER_ICON,
      storage: await toConnectorData(session, this),
    }
  }

  async setWebsiteMeta(session: any, id: string, data: WebsiteMetaFileContent): Promise<void> {
    const websiteId = requiredParam<WebsiteId>(id, 'website id')
    const content = JSON.stringify(data)
    const path = join(this.options.path, id, WEBSITE_META_DATA_FILE)
    await fs.writeFile(path, content)
  }

  async getWebsiteMeta(session: FsSession, id: WebsiteId): Promise<WebsiteMeta> {
    const websiteId = requiredParam<WebsiteId>(id, 'website id')
    // Get stats for website folder
    const fileStat = await fs.stat(join(this.options.path, websiteId))
    const path = join(this.options.path, websiteId, WEBSITE_META_DATA_FILE)
    // Get meta file
    const content = await fs.readFile(path)
    const meta = await JSON.parse(content.toString())
    // Return all meta
    return {
      websiteId,
      name: meta.name,
      imageUrl: meta.imageUrl,
      connectorUserSettings: meta.connectorUserSettings,
      createdAt: fileStat.birthtime,
      updatedAt: fileStat.mtime,
    }
  }

  // ********************
  // Storage interface
  // ********************
  async createWebsite(session: FsSession, meta: WebsiteMetaFileContent): Promise<WebsiteId> {
    const id = uuid()
    await fs.mkdir(join(this.options.path, id, this.options.assetsFolder), { recursive: true })
    await this.setWebsiteMeta(session, id, meta)
    await this.updateWebsite(session, id, defaultWebsiteData)
    return id
  }

  async readWebsite(session: FsSession, websiteId: WebsiteId): Promise<WebsiteData> {
    const id = requiredParam<WebsiteId>(websiteId, 'website id')
    const path = join(this.options.path, id, WEBSITE_DATA_FILE)
    const content = await fs.readFile(path)
    return JSON.parse(content.toString())
  }

  async updateWebsite(session: FsSession, websiteId: WebsiteId, data: WebsiteData): Promise<void> {
    const id = requiredParam<WebsiteId>(websiteId, 'website id')
    const path = join(this.options.path, id, WEBSITE_DATA_FILE)
    await fs.writeFile(path, JSON.stringify(data))
  }

  async deleteWebsite(session: FsSession, websiteId: WebsiteId): Promise<void> {
    const id = requiredParam<WebsiteId>(websiteId, 'website id')
    const path = join(this.options.path, id)
    return fs.rmdir(path, { recursive: true })
  }

  async duplicateWebsite(session: FsSession, websiteId: WebsiteId): Promise<void> {
    const newWebsiteId = uuid()
    const from = join(this.options.path, websiteId)
    const to = join(this.options.path, newWebsiteId)
    await copyDir(from, to)
    const meta = await this.getWebsiteMeta(session, websiteId)
    await this.setWebsiteMeta(session, newWebsiteId, {
      ...meta,
      name: `${meta.name} copy`,
    })
  }

  async listWebsites(session: any): Promise<WebsiteMeta[]> {
    const list = await fs.readdir(this.options.path)
    return Promise.all(list.map(async fileName => {
      const websiteId = fileName as WebsiteId
      return this.getWebsiteMeta(session, websiteId)
    }))
  }

  async getAsset(session: FsSession, id: WebsiteId, path: string): Promise<ConnectorFile> {
    const fullPath = join(this.options.path, id, this.options.assetsFolder, path)
    const content = await fs.readFile(fullPath)
    return { path, content }
  }

  async writeAssets(session: FsSession, id: WebsiteId, files: ConnectorFile[], statusCbk?: StatusCallback): Promise<void> {
    return this.write(session, id, files, this.options.assetsFolder, statusCbk)
  }

  async write(session: FsSession, id: WebsiteId, files: ConnectorFile[], assetsFolder: string, statusCbk?: StatusCallback): Promise<void> {
    const filesStatuses = this.initStatus(files)
    let error: Error | null = null
    for (const fileStatus of filesStatuses) {
      const {file} = fileStatus
      const path = join(this.options.path, id, assetsFolder, file.path)
      if (typeof file.content === 'string' || Buffer.isBuffer(file.content)) {
        fileStatus.message = 'Writing'
        this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
        try {
          await fs.writeFile(path, file.content)
        } catch(err) {
          fileStatus.message = `Error (${err})`
          this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
          error = err
          continue
        }
        fileStatus.message = 'Success'
        this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
      } else if (file.content instanceof Readable) {
        fileStatus.message = 'Writing'
        this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
        const writeStream = createWriteStream(path)
        file.content.pipe(writeStream)
        await new Promise((resolve) => {
          writeStream.on('finish', () => {
            fileStatus.message = 'Success'
            this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
            resolve(file)
          })
          writeStream.on('error', err => {
            console.error('writeStream error', err)
            fileStatus.message = `Error (${err})`
            this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
            error = err
            resolve(file)
          })
        })
      } else {
        console.error('Invalid file content', typeof file.content)
        throw new Error('Invalid file content: ' + typeof file.content)
      }
    }
    this.updateStatus(filesStatuses, error ? JobStatus.ERROR : JobStatus.SUCCESS, statusCbk)
    if(error) throw error
  }

  async deleteAssets(session: FsSession, id: WebsiteId, paths: string[]): Promise<void> {
    for (const path of paths) {
      await fs.unlink(join(this.options.path, id, path))
    }
  }

  async readAsset(session: object, websiteId: string, fileName: string): Promise<ConnectorFileContent> {
    const id = requiredParam<WebsiteId>(websiteId, 'website id')
    const path = join(this.options.path, id, this.options.assetsFolder, fileName)
    return await fs.readFile(path)
  }
}
