import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { PDFDocument, StandardFonts, rgb, PDFTextField, PDFCheckBox } from 'pdf-lib'
import {
  FormTemplate, FormTemplateDocument,
  FormGenerationLog, FormGenerationLogDocument,
  ClientFolder, ClientFolderDocument,
  Matter, MatterDocument,
  LegalDocument, LegalDocumentDocument,
} from '../common/schemas'
import { CreateFormTemplateDto } from './dto/create-form-template.dto'
import { UpdateFormTemplateDto } from './dto/update-form-template.dto'
import { resolveHtmlTemplate } from './html-templates/shared'
import { HTML_TEMPLATES } from './html-templates/corporate-annual-compliance'
import * as fs from 'fs'
import * as path from 'path'

// ─── Data source resolver ─────────────────────────────────────────────────────

function resolveDataSource(
  source: string,
  client: any,
  matter: any,
  staticValue?: string,
  dateFormat?: string,
): string {
  if (source === 'static') return staticValue ?? ''

  const [ns, ...rest] = source.split('.')
  const key = rest.join('.')

  let raw: any
  if (ns === 'client') raw = client[key]
  else if (ns === 'matter') raw = matter[key]
  else return ''

  if (raw === undefined || raw === null) return ''

  // Date formatting
  if (raw instanceof Date || (typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}/.test(raw))) {
    const d = new Date(raw)
    if (!isNaN(d.getTime())) {
      const fmt = dateFormat ?? 'DD/MM/YYYY'
      const dd = String(d.getDate()).padStart(2, '0')
      const mm = String(d.getMonth() + 1).padStart(2, '0')
      const yyyy = String(d.getFullYear())
      return fmt.replace('DD', dd).replace('MM', mm).replace('YYYY', yyyy).replace('YY', yyyy.slice(2))
    }
  }

  return String(raw)
}

// ─── PDF fetcher ──────────────────────────────────────────────────────────────
// Downloads a PDF from a URL and caches it locally.
// Stock forms are cached under uploads/form-templates/stock/<filename>.

async function fetchAndCachePdf(
  template: FormTemplateDocument,
  templateModel: Model<FormTemplateDocument>,
): Promise<Buffer> {
  const stockDir = path.join(process.cwd(), 'uploads', 'form-templates', 'stock')
  if (!fs.existsSync(stockDir)) fs.mkdirSync(stockDir, { recursive: true })

  // Derive a stable filename from stockId or _id
  const safeId = ((template as any).stockId ?? String((template as any)._id)).replace(/[^a-zA-Z0-9_-]/g, '_')
  const cachedPath = path.join(stockDir, `${safeId}.pdf`)

  // Return cached copy if present and non-empty
  if (fs.existsSync(cachedPath) && fs.statSync(cachedPath).size > 0) {
    return fs.readFileSync(cachedPath)
  }

  const urls = [
    (template as any).sourceUrl,
    ...((template as any).fallbackUrls ?? []),
  ].filter(Boolean)

  if (!urls.length) throw new BadRequestException(`No sourceUrl for template ${template.name}`)

  let lastErr: any
  for (const url of urls) {
    try {
      const res = await fetch(url, { signal: AbortSignal.timeout(15_000) })
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const bytes = Buffer.from(await res.arrayBuffer())
      fs.writeFileSync(cachedPath, bytes)
      // Update storageKey on the document so future calls skip the fetch
      await templateModel.findByIdAndUpdate((template as any)._id, { storageKey: `stock/${safeId}.pdf` })
      return bytes
    } catch (e) {
      lastErr = e
    }
  }
  throw new BadRequestException(`Could not fetch PDF for "${template.name}": ${lastErr?.message}`)
}

// ─── Service ──────────────────────────────────────────────────────────────────

@Injectable()
export class FormTemplatesService {
  constructor(
    @InjectModel(FormTemplate.name)      private templateModel:  Model<FormTemplateDocument>,
    @InjectModel(FormGenerationLog.name) private logModel:       Model<FormGenerationLogDocument>,
    @InjectModel(ClientFolder.name)      private clientModel:    Model<ClientFolderDocument>,
    @InjectModel(Matter.name)            private matterModel:    Model<MatterDocument>,
    @InjectModel(LegalDocument.name)     private docModel:       Model<LegalDocumentDocument>,
  ) {}

  // ── CRUD ────────────────────────────────────────────────────────────────────

  async create(dto: CreateFormTemplateDto, createdBy?: string): Promise<FormTemplateDocument> {
    return this.templateModel.create({ ...dto, createdBy })
  }

  async findAll(practiceArea?: string, includeStock = true): Promise<FormTemplateDocument[]> {
    const filter: any = { isActive: true }
    if (practiceArea) filter.practiceArea = { $in: [practiceArea, 'universal'] }
    if (!includeStock) filter.isSystem = { $ne: true }
    return this.templateModel.find(filter).sort({ isSystem: -1, name: 1 }).lean() as any
  }

  async findOne(id: string): Promise<FormTemplateDocument> {
    const doc = await this.templateModel.findById(id).lean() as any
    if (!doc) throw new NotFoundException(`FormTemplate ${id} not found`)
    return doc
  }

  async update(id: string, dto: UpdateFormTemplateDto): Promise<FormTemplateDocument> {
    const doc = await this.templateModel.findByIdAndUpdate(id, dto, { new: true }).lean() as any
    if (!doc) throw new NotFoundException(`FormTemplate ${id} not found`)
    return doc
  }

  async remove(id: string): Promise<void> {
    const doc = await this.templateModel.findById(id)
    if (!doc) throw new NotFoundException(`FormTemplate ${id} not found`)
    if ((doc as any).isSystem) throw new BadRequestException('System form templates cannot be deleted')
    await doc.deleteOne()
  }

  // ── PDF Generation ──────────────────────────────────────────────────────────

  async generate(
    templateId: string,
    matterId: string,
    generatedBy?: string,
  ): Promise<{ buffer: Buffer; filename: string; logId: string }> {
    const template = await this.templateModel.findById(templateId)
    if (!template) throw new NotFoundException(`FormTemplate ${templateId} not found`)

    const matter = await this.matterModel.findById(matterId).lean()
    if (!matter) throw new NotFoundException(`Matter ${matterId} not found`)

    const client = await this.clientModel.findById((matter as any).clientFolderId).lean()
    if (!client) throw new NotFoundException(`ClientFolder for matter ${matterId} not found`)

    // ── html-template mode: delegate to generateFromHtml ────────────────────
    const fillMode: string = (template as any).fillMode ?? 'coordinate'
    if (fillMode === 'html-template') {
      // Caller must provide a data map via generateFromHtml(); calling generate()
      // directly on an html-template form will throw a clear error.
      throw new BadRequestException(
        `Template "${template.name}" uses html-template fill mode. ` +
        `Call generateFromHtml(templateId, dataMap, matterId) instead, ` +
        `or use the annual-compliance bulk-generate endpoint.`
      )
    }

    // ── Load blank PDF ──────────────────────────────────────────────────────
    let pdfBytes: Buffer

    const storageKey = (template as any).storageKey
    if (storageKey) {
      // Already cached / uploaded
      const localPath = path.join(process.cwd(), 'uploads', 'form-templates', storageKey)
      if (fs.existsSync(localPath)) {
        pdfBytes = fs.readFileSync(localPath)
      } else {
        // storageKey on record but file missing — re-fetch
        pdfBytes = await fetchAndCachePdf(template, this.templateModel)
      }
    } else if ((template as any).sourceUrl) {
      // Stock form — fetch & cache
      pdfBytes = await fetchAndCachePdf(template, this.templateModel)
    } else {
      throw new BadRequestException(`No PDF source for template "${template.name}". Upload a blank PDF first.`)
    }

    const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true })

    // ── Fill fields ─────────────────────────────────────────────────────────
    if (fillMode === 'acroform') {
      // Use PDF's built-in form fields
      const form = pdfDoc.getForm()
      for (const field of (template as any).fields) {
        const value = resolveDataSource(field.dataSource, client, matter, field.staticValue, field.dateFormat)
        if (!value || !field.acroFieldName) continue
        const truncated = field.maxLength ? value.slice(0, field.maxLength) : value
        try {
          const pdfField = form.getField(field.acroFieldName)
          if (pdfField instanceof PDFTextField) {
            pdfField.setText(truncated)
          } else if (pdfField instanceof PDFCheckBox) {
            if (truncated === 'true' || truncated === '1' || truncated === 'yes') pdfField.check()
          }
        } catch {
          // Field not found in this PDF version — skip silently
        }
      }
      // Flatten form so values are baked in and not editable
      try { form.flatten() } catch { /* some PDFs resist flattening */ }

    } else {
      // Coordinate-based text overlay
      const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
      const pages = pdfDoc.getPages()

      for (const field of (template as any).fields) {
        const value = resolveDataSource(field.dataSource, client, matter, field.staticValue, field.dateFormat)
        if (!value) continue
        const truncated = field.maxLength ? value.slice(0, field.maxLength) : value

        const pageIndex = ((field.page ?? 1) - 1)
        if (pageIndex < 0 || pageIndex >= pages.length) continue

        pages[pageIndex].drawText(truncated, {
          x:    field.x    ?? 0,
          y:    field.y    ?? 0,
          size: field.fontSize ?? 10,
          font: helvetica,
          color: rgb(0, 0, 0),
        })
      }
    }

    const filledBytes = await pdfDoc.save()
    const buffer = Buffer.from(filledBytes)

    return this.saveAndLog({ buffer, template, matter, matterId, generatedBy })
  }

  // ── HTML template generation ──────────────────────────────────────────────

  /**
   * Generate a PDF from an HTML template using headless Chrome (Puppeteer).
   * The data map is a flat key→value record resolved by the caller
   * (annual-compliance service pre-resolves all fields including per-director/
   * per-shareholder context before calling this).
   */
  async generateFromHtml(
    templateId: string,
    data: Record<string, string>,
    matterId: string,
    generatedBy?: string,
    /** Optional pre-opened browser page — pass when rendering multiple docs in a batch */
    page?: import('puppeteer').Page,
  ): Promise<{ buffer: Buffer; filename: string; logId: string }> {
    const template = await this.templateModel.findById(templateId)
    if (!template) throw new NotFoundException(`FormTemplate ${templateId} not found`)

    const matter = await this.matterModel.findById(matterId).lean()
    if (!matter) throw new NotFoundException(`Matter ${matterId} not found`)

    const stockId: string = (template as any).stockId ?? ''
    const htmlTemplate = HTML_TEMPLATES[stockId]
    if (!htmlTemplate) {
      throw new BadRequestException(
        `No HTML template registered for stockId "${stockId}". ` +
        `Ensure the template is listed in HTML_TEMPLATES.`
      )
    }

    const html = resolveHtmlTemplate(htmlTemplate, data)

    let buffer: Buffer
    if (page) {
      // Reuse the caller's browser page (batch render — fast)
      buffer = await this.htmlToPdfWithPage(page, html)
    } else {
      // Standalone call — open and close our own browser
      const browser = await this.openBrowser()
      try {
        const p = await browser.newPage()
        buffer = await this.htmlToPdfWithPage(p, html)
        await p.close()
      } finally {
        await browser.close()
      }
    }

    return this.saveAndLog({ buffer, template, matter, matterId, generatedBy })
  }

  /**
   * Render one HTML string to a PDF buffer using the supplied browser page.
   * Caller is responsible for managing the browser lifecycle.
   * Using a single browser across all docs in a batch cuts generation time
   * from ~5s/doc (cold launch) to ~1s/doc (warm page).
   */
  async htmlToPdfWithPage(
    page: import('puppeteer').Page,
    html: string,
  ): Promise<Buffer> {
    await page.setContent(html, { waitUntil: 'load', timeout: 30_000 })
    const pdfBuffer = await page.pdf({
      format: 'A4',
      printBackground: true,
      margin: { top: '22mm', bottom: '22mm', left: '20mm', right: '20mm' },
    })
    return Buffer.from(pdfBuffer)
  }

  /** Open a single headless Chrome browser for a batch of HTML-to-PDF renders. */
  async openBrowser(): Promise<import('puppeteer').Browser> {
    const puppeteer = await import('puppeteer')
    return puppeteer.default.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
    })
  }

  /** Shared file-save + audit-log step used by both generate paths. */
  private async saveAndLog(opts: {
    buffer: Buffer
    template: FormTemplateDocument
    matter: any
    matterId: string
    generatedBy?: string
  }): Promise<{ buffer: Buffer; filename: string; logId: string }> {
    const { buffer, template, matter, matterId, generatedBy } = opts

    // ── Output filename ──────────────────────────────────────────────────────
    const today     = new Date().toISOString().split('T')[0]
    const safeName  = template.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40)
    const matterNum = (matter as any).matterNumber ?? matterId
    const filename  = `${safeName}_${matterNum}_${today}.pdf`

    // ── Save generated file ──────────────────────────────────────────────────
    const outputDir = path.join(process.cwd(), 'uploads', 'generated-forms', matterId)
    if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true })
    fs.writeFileSync(path.join(outputDir, filename), buffer)

    // ── Audit log ────────────────────────────────────────────────────────────
    const log = await this.logModel.create({
      formTemplateId:   String((template as any)._id),
      formTemplateName: template.name,
      matterId,
      clientFolderId:   String((matter as any).clientFolderId),
      storageKey:       `${matterId}/${filename}`,
      generatedBy,
    })

    return { buffer, filename, logId: String((log as any)._id) }
  }

  // ── Library management ───────────────────────────────────────────────────────

  async reseedLibrary(): Promise<{ inserted: number; skipped: number }> {
    const { STOCK_FORMS } = await import('./stock-library')
    let inserted = 0
    let skipped = 0
    for (const form of STOCK_FORMS) {
      const exists = await this.templateModel.exists({ stockId: form.stockId })
      if (exists) { skipped++; continue }
      await this.templateModel.create({
        name: form.name, description: form.description,
        practiceArea: form.practiceArea, sourceUrl: form.sourceUrl,
        fillMode: form.fillMode, fields: form.fields,
        isActive: true, isSystem: true,
        stockId: form.stockId, needsCalibration: form.needsCalibration,
        createdBy: 'system',
      })
      inserted++
    }
    return { inserted, skipped }
  }

  async clearCachedPdf(id: string): Promise<void> {
    const doc = await this.templateModel.findById(id)
    if (!doc) throw new NotFoundException(`FormTemplate ${id} not found`)
    const sk = (doc as any).storageKey
    if (sk && sk.startsWith('stock/')) {
      const p = path.join(process.cwd(), 'uploads', 'form-templates', sk)
      if (fs.existsSync(p)) fs.unlinkSync(p)
    }
    await this.templateModel.findByIdAndUpdate(id, { storageKey: '' })
  }

  // ── Generation logs ─────────────────────────────────────────────────────────

  async listLogs(matterId: string): Promise<FormGenerationLogDocument[]> {
    return this.logModel.find({ matterId }).sort({ createdAt: -1 }).lean() as any
  }

  // ── Stock library admin ──────────────────────────────────────────────────────

  async createCustomForm(opts: {
    name: string
    description?: string
    practiceArea: string
    storageKey: string
    originalFilename?: string
  }): Promise<FormTemplateDocument> {
    return this.templateModel.create({
      name:             opts.name,
      description:      opts.description,
      practiceArea:     opts.practiceArea,
      storageKey:       opts.storageKey,
      originalFilename: opts.originalFilename,
      fillMode:         'coordinate',
      fields:           [],
      isSystem:         false,
      isActive:         true,
      needsCalibration: true,
      createdBy:        'tenant-upload',
    })
  }

  async getRawPdf(id: string): Promise<{ buffer: Buffer; filename: string }> {
    const template = await this.templateModel.findById(id)
    if (!template) throw new NotFoundException(`FormTemplate ${id} not found`)

    const storageKey = (template as any).storageKey
    if (!storageKey) throw new BadRequestException(`No PDF uploaded yet for "${template.name}"`)

    const localPath = path.join(process.cwd(), 'uploads', 'form-templates', storageKey)
    if (!fs.existsSync(localPath)) throw new BadRequestException(`PDF file not found on disk for "${template.name}"`)

    const buffer = fs.readFileSync(localPath)
    const filename = `${template.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.pdf`
    return { buffer, filename }
  }

  async findByStockId(stockId: string): Promise<FormTemplateDocument | null> {
    return this.templateModel.findOne({ stockId }).lean() as any
  }

  async updateStockFormStorage(stockId: string, storageKey: string): Promise<void> {
    await this.templateModel.findOneAndUpdate(
      { stockId },
      { storageKey, needsCalibration: true },
    )
  }
}
