File size: 31,961 Bytes
402122a
 
 
 
 
 
 
 
 
 
 
 
d5e836f
402122a
 
 
 
 
 
 
de0b6bd
402122a
 
 
450b2cd
402122a
 
 
57e5629
402122a
 
d5e836f
402122a
5ab65a7
d5e836f
402122a
de0b6bd
 
 
8dc1ac9
de0b6bd
ec8d47f
 
 
 
de0b6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
add8107
de0b6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
add8107
de0b6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bebdbf4
de0b6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06c3356
de0b6bd
 
bebdbf4
de0b6bd
06c3356
de0b6bd
 
 
 
 
 
 
bebdbf4
de0b6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402122a
 
 
 
8dc1ac9
402122a
ec8d47f
 
 
 
402122a
 
d734bf7
 
 
 
402122a
 
 
de0b6bd
 
 
 
 
402122a
de0b6bd
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92b8dbd
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
88a3921
 
7b50d28
 
 
88a3921
 
 
 
 
 
 
 
 
 
 
 
 
402122a
 
 
 
88a3921
402122a
 
88a3921
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
add8107
402122a
 
 
 
 
 
 
d734bf7
 
 
 
 
 
 
 
 
402122a
 
 
 
 
06c3356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
add8107
402122a
 
 
 
 
 
 
 
 
 
92b8dbd
402122a
06c3356
402122a
06c3356
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bebdbf4
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06c3356
402122a
bebdbf4
 
402122a
 
 
 
 
 
 
 
 
bebdbf4
402122a
 
 
 
 
 
bebdbf4
402122a
 
 
 
 
 
 
d734bf7
 
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de0b6bd
402122a
 
 
 
 
 
 
 
 
 
 
 
 
92b8dbd
402122a
 
 
 
 
 
 
 
 
 
 
 
92b8dbd
402122a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
# PDF Generator Module
# Contains create_pdf_document() and all PDF-related functions

import re
import os
import tempfile
import time
import sys
from typing import List, Dict, Any

# Add the parent directory to sys.path to import from models.py
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from models import ExportRequest

# Import from the same directory (Document Generation)
from document_helpers import (
    filter_empty_sections,
    extract_business_name_from_content,
    remove_duplicate_headings
)
from table_of_content import generate_table_of_contents_after_content, add_table_of_contents_page
from chart_generator import parse_embedded_chart_data, create_chart_from_data
from markdown_renderer import (
    clean_text_for_pdf,
    render_markdown_to_pdf_grouped
)

# Add Currency Mapping path
currency_mapping_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'CurrencyMapping')
if currency_mapping_path not in sys.path:
    sys.path.append(currency_mapping_path)
from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso

# Import sort_sections_by_order from utils module
from utils import sort_sections_by_order

def calculate_real_page_numbers(request: ExportRequest, filtered_sections):
    """Calculate real page numbers by creating a temporary PDF with all content INCLUDING charts"""
    try:
        from fpdf2 import FPDF
    except ImportError:
        try:
            from fpdf import FPDF
        except ImportError:
            raise Exception("Neither fpdf2 nor fpdf package found. Please install fpdf2.")
    
    # Create temporary PDF to calculate page numbers
    temp_pdf = FPDF()
    temp_pdf.set_auto_page_break(auto=True, margin=15)
    
    # Add cover page
    temp_pdf.add_page()
    
    # Add TOC page (placeholder)
    temp_pdf.add_page()
    
    # Parse charts from embedded chart data in section content
    embedded_charts = parse_embedded_chart_data(filtered_sections)
    
    # Create charts and map them to sections
    chart_info_map = {}
    if embedded_charts:
        for chart_info in embedded_charts:
            chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
            if chart_bytes:
                source_section = chart_info["source_section"]
                if source_section not in chart_info_map:
                    chart_info_map[source_section] = []
                chart_info_map[source_section].append({
                    "chart_bytes": chart_bytes,
                    "title": chart_info["title"],
                    "type": chart_info["type"]
                })
    
    # Add all sections to calculate real page numbers INCLUDING charts
    section_page_map = {}
    sorted_sections = sort_sections_by_order(filtered_sections)
    
    for section in sorted_sections:
        temp_pdf.add_page()
        current_page = temp_pdf.page_no()
        section_page_map[section.title] = current_page
        
        # Don't add section title manually - let the markdown content handle headings
        
        # Add section content
        temp_pdf.set_font("Arial", "", 12)
        temp_pdf.set_text_color(51, 51, 51)
        temp_pdf.set_xy(25, temp_pdf.get_y())
        
        # Process content
        try:
            content_text = clean_text_for_pdf(section.content)
        except Exception as e:
            content_text = section.content or ""
        
        # Remove chart data markers
        content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
        content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
        
        # No need to filter section titles since we're not adding them manually
        
        # Add content with proper markdown rendering
        if content_text and content_text.strip():
            render_markdown_to_pdf_grouped(temp_pdf, content_text, (0,0,0), (75,0,130), (51,51,51))
        
        # Add charts for this section (same logic as in actual PDF generation)
        section_charts = chart_info_map.get(section.title, [])
        
        # Try different matching strategies for charts
        if not section_charts:
            # Case-insensitive match
            for chart_source, charts in chart_info_map.items():
                if chart_source.lower() == section.title.lower():
                    section_charts = charts
                    break
            
            # Partial match
            if not section_charts:
                for chart_source, charts in chart_info_map.items():
                    if (section.title.lower() in chart_source.lower() or 
                        chart_source.lower() in section.title.lower()):
                        section_charts = charts
                        break
            
            # Key-based match
            if not section_charts and hasattr(section, 'key'):
                for chart_source, charts in chart_info_map.items():
                    if section.key and section.key.replace('prompt_', '') in chart_source.lower():
                        section_charts = charts
                        break
        
        # Add charts with same page break logic as actual PDF
        if section_charts:
            charts_per_page = 0
            max_charts_per_page = 5
            
            for chart_info in section_charts:
                try:
                    chart_height_needed = 140  # Chart height (120) + title (20) + minimal spacing
                    current_y = temp_pdf.get_y()
                    charts_per_page += 1
                    
                    # Only force page break if absolutely necessary
                    if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
                        temp_pdf.add_page()
                        y_title = 25
                        charts_per_page = 1
                    else:
                        y_title = current_y
                    
                    # Add minimal spacing before chart if not at top of page
                    if y_title > 25:
                        temp_pdf.ln(3)
                    
                    # Chart title is already included in the chart itself, no need for separate title
                    
                    # Add chart image
                    img_width = 160  # Much larger chart width
                    x = (210 - img_width) / 2
                    y_img = y_title
                    
                    # Save image to temp file
                    with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
                        tmp_img.write(chart_info["chart_bytes"])
                        temp_img_path = tmp_img.name
                    
                    # Add chart
                    chart_height = 120  # Much larger chart height
                    temp_pdf.image(temp_img_path, x=x, y=y_img, w=img_width)
                    temp_pdf.ln(chart_height + 5)
                    
                    # Clean up temp file
                    try:
                        os.remove(temp_img_path)
                    except Exception:
                        pass
                        
                except Exception:
                    # Skip faulty chart and continue
                    continue
    
    return section_page_map

def create_pdf_document(request: ExportRequest):
    """Create a PROFESSIONAL PDF document from the business plan data using FPDF2
    NOTE: Now uses matplotlib charts exclusively - old base64 chart logic commented out"""
    try:
        from fpdf2 import FPDF
    except ImportError:
        try:
            from fpdf import FPDF
        except ImportError:
            raise Exception("Neither fpdf2 nor fpdf package found. Please install fpdf2.")
    
    # Filter out empty sections to prevent blank pages
    try:
        filtered_sections = filter_empty_sections(request.sections)
    except Exception as e:
        raise Exception(f"Failed to filter sections: {str(e)}")
    if not filtered_sections:
        raise Exception("No valid sections found after filtering. All sections appear to be empty.")
    
    # First pass: Calculate real page numbers by creating a temporary PDF
    section_page_map = calculate_real_page_numbers(request, filtered_sections)
    
    # Generate TOC with real page numbers
    toc_items = generate_table_of_contents_after_content(filtered_sections, section_page_map, request.businessIdea)
    
    # Second pass: Create final PDF with TOC on page 2
    pdf = FPDF()
    pdf.set_auto_page_break(auto=True, margin=15)  # Reduced margin to allow more charts per page
    
    # Add confidentiality footer to every page
    def add_confidentiality_footer(pdf):
        pdf.set_font("Arial", "", 8)
        pdf.set_text_color(100, 100, 100)  # Gray text
        pdf.set_y(-15)  # 15mm from bottom
        pdf.cell(0, 5, "CONFIDENTIAL - This document contains proprietary information and may not be shared without consent", 0, 0, 'C')
    
    # Override the footer method
    pdf.footer = lambda: add_confidentiality_footer(pdf)
    pdf.add_page()
    
    # Professional color scheme - Refined for bank/investor presentation
    primary_color = (0, 0, 0)        # Black - for headings
    accent_color = (75, 0, 130)      # Navy purple - for accent lines and highlights
    text_color = (51, 51, 51)        # Dark gray - for body text
    light_gray = (221, 221, 221)     # Lighter gray - for subtle lines (#DDDDDD)
    navy_color = (25, 25, 112)       # Navy blue - for TOC and emphasis
    
    # Professional fonts - Arial for body, optional serif for main titles
    title_font = 'Arial'  # Keep Arial for consistency
    body_font = 'Arial'   # Modern business feel
    
    # Helper functions for consistent styling
    def add_header(pdf, text, size=24, y_offset=25, primary_color=(0,0,0), accent_color=(75,0,130)):
        pdf.set_font("Arial", "B", size)
        pdf.set_text_color(*primary_color)
        pdf.set_xy(25, y_offset)
        pdf.cell(0, 18, text, ln=True)
        
        # Calculate text width and make line autofit
        text_width = pdf.get_string_width(text)
        line_width = min(text_width + 10, 160)  # Add 10mm padding, max 160mm (page width - margins)
        
        # Removed purple accent line
        return y_offset + 25

    def add_subheader(pdf, text, size=16, y_offset=None, primary_color=(0,0,0)):
        if y_offset is None:
            y_offset = pdf.get_y() + 10
        pdf.set_font("Arial", "B", size)
        pdf.set_text_color(*primary_color)
        pdf.set_xy(25, y_offset)
        pdf.cell(0, 14, text, ln=True)
        
        # Calculate text width and make line autofit
        text_width = pdf.get_string_width(text)
        line_width = min(text_width + 8, 160)  # Add 8mm padding, max 160mm (page width - margins)
        
        pdf.set_draw_color(221, 221, 221)  # Lighter gray (#DDDDDD) for subtle lines
        pdf.set_line_width(0.3)  # Thinner line
        pdf.line(25, y_offset + 14, 25 + line_width, y_offset + 14)
        return y_offset + 20

    def add_body_text(pdf, text, y_offset=None, text_color=(51,51,51)):
        if y_offset is None:
            y_offset = pdf.get_y() + 5
        pdf.set_font("Arial", "", 12)
        pdf.set_text_color(*text_color)
        pdf.set_xy(25, y_offset)
        pdf.multi_cell(160, 8, text, align='L')  # Left align to prevent text justification
        return pdf.get_y()

    def check_page_break(pdf, required_height=120, bottom_margin=300):
        """Check if we need a page break and return the Y position to use"""
        y = pdf.get_y()
        if y + required_height > bottom_margin:
            pdf.add_page()
            return 25
        return y

    # ===== COVER PAGE =====
    def add_cover_page():
        # Clean white background - no purple background
        
        # Add company logo at the top
        try:
            # Use the MBD logo PNG file - now in app folder for HF deployment
            app_root = os.path.dirname(os.path.dirname(__file__))
            logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png')
            
            if os.path.exists(logo_path):
                # Add logo to PDF (centered, top of page) - LOGO FIRST, THEN TITLE BELOW
                # Center the logo: (210 - 70) / 2 = 70, so logo starts at x=70
                pdf.image(logo_path, x=70, y=20, w=70)  # Even larger size to show full logo
                print(f"Logo added successfully from: {logo_path}")
            else:
                print(f"Logo file not found at: {logo_path}")
        except Exception as e:
            # Log any errors with logo
            print(f"Logo error: {e}")
            pass
        
        # Extract business name from content, fallback to businessIdea
        extracted_business_name = extract_business_name_from_content(filtered_sections)
        business_name = extracted_business_name or request.businessIdea or "Business Plan"
        
        # Company/Project name - DARK TEXT (moved down below logo)
        pdf.set_font(title_font, "B", 32)
        pdf.set_text_color(0, 0, 0)  # Black text for visibility on white background
        pdf.ln(60)  # More space to move title below logo
        pdf.cell(0, 25, business_name, ln=True, align='C')
        
        # Subtitle - DARK GRAY, BOLD, NO ITALICS
        pdf.set_font(title_font, "B", 16)  # Reduced from 18pt, removed italics
        pdf.set_text_color(51, 51, 51)  # Dark gray for visibility on white background
        pdf.cell(0, 12, "Business Plan", ln=True, align='C')
        
        # Add some space
        pdf.ln(40)
    
        
        pdf.set_font(body_font, "", 12)
        pdf.set_text_color(51, 51, 51)  # Dark text for content
        pdf.set_xy(35, 145)
        pdf.cell(0, 8, f"Created: {time.strftime('%B %d, %Y')}", ln=True)
        pdf.set_xy(35, 153)
        pdf.cell(0, 8, f"Format: {request.format.upper()}", ln=True)
        pdf.set_xy(35, 161)
        pdf.cell(0, 8, f"Sections: {len(request.sections)}", ln=True)
        
        # Footer - simple footer since confidentiality is now in page footer
        pdf.set_font(body_font, "", 10)
        pdf.set_text_color(51, 51, 51)  # Dark gray text for visibility on white background
        pdf.set_xy(0, 280)
        pdf.cell(0, 8, "Business Plan Document", ln=True, align='C')
    

    
    # ===== SECTION PAGES =====
    def add_sections_with_charts():
        # Parse charts from embedded chart data in section content
        embedded_charts = parse_embedded_chart_data(filtered_sections)
        
        if embedded_charts:
            # Create actual charts from the parsed data
            matplotlib_charts = []
            chart_info_map = {}  # Map section names to their charts
            
            # logging.info(f"Chart info map keys: {list(chart_info_map.keys())}")
            
            for chart_info in embedded_charts:
                chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
                if chart_bytes:
                    matplotlib_charts.append(chart_bytes)
                    # Map this chart to its source section
                    source_section = chart_info["source_section"]
                    # logging.info(f"Chart '{chart_info['title']}' has source_section: '{source_section}'")
                    
                    if source_section not in chart_info_map:
                        chart_info_map[source_section] = []
                    chart_info_map[source_section].append({
                        "chart_bytes": chart_bytes,
                        "title": chart_info["title"],
                        "type": chart_info["type"]
                    })
                    # logging.info(f"Mapping chart '{chart_info['title']}' to source section: '{source_section}'")
                    
                    # CRITICAL DEBUG: Show the exact mapping being created
                    # logging.info(f"  -> Chart '{chart_info['title']}' mapped to key '{source_section}' in chart_info_map")
                else:
                    pass
            
            # logging.info(f"Total charts created: {len(matplotlib_charts)}")
            # logging.info(f"Final chart_info_map: {chart_info_map}")
            
            # Debug: Show all chart mappings
            for source_section, charts in chart_info_map.items():
                # logging.info(f"Source section '{source_section}' has {len(charts)} charts:")
                for chart in charts:
                    # logging.info(f"  - {chart['title']} ({chart['type']})")
                    pass
        
        else:
            matplotlib_charts = []
            chart_info_map = {}
        
        total_charts_placed = 0
        
        # Sort sections according to predefined order
        sorted_sections = sort_sections_by_order(filtered_sections)
        # logging.info(f"Section titles in sorted order: {[s.title for s in sorted_sections]}")
        
        # CRITICAL DEBUG: Show exact section titles and their keys
        for i, section in enumerate(sorted_sections):
            # logging.info(f"Section {i+1}: '{section.title}' (key: {getattr(section, 'key', 'NO_KEY')})")
            pass
        
        for i, section in enumerate(sorted_sections):
            
            # Always start each section on a new page
            pdf.add_page()
            
            # Don't add section title manually - let the markdown content handle headings
            
            # Section content (properly positioned)
            pdf.set_font(body_font, "", 12)
            pdf.set_text_color(*text_color)
            pdf.set_xy(25, pdf.get_y())  # Use current Y position for consistent spacing
            
            # Process content
            try:
                content_text = clean_text_for_pdf(section.content)
            except Exception as e:
                content_text = section.content or ""
            try:
                content_text = convert_currency_symbols_to_iso(content_text)
            except Exception:
                # If currency conversion fails, keep original text
                pass
            
            # Remove chart data markers
            content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
            content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
            
            # Remove chart titles from content to prevent duplicate headings
            # Get chart titles for this section
            section_charts = chart_info_map.get(section.title, [])
            if not section_charts:
                # Try case-insensitive match
                for chart_source, charts in chart_info_map.items():
                    if chart_source.lower() == section.title.lower():
                        section_charts = charts
                        break
            
            # Remove chart titles from content
            for chart_info in section_charts:
                chart_title = chart_info["title"]
                # Remove various markdown heading formats for the chart title
                patterns_to_remove = [
                    rf'^#+\s*{re.escape(chart_title)}\s*$',  # # Title, ## Title, etc.
                    rf'^\*\*{re.escape(chart_title)}\*\*\s*$',  # **Title**
                    rf'^{re.escape(chart_title)}\s*$',  # Just the title
                ]
                for pattern in patterns_to_remove:
                    content_text = re.sub(pattern, '', content_text, flags=re.MULTILINE | re.IGNORECASE)
            
            # No need to filter section titles since we're not adding them manually
            
            # Skip enhance_subheadings_with_bold for grouped rendering to preserve ** markers
            # content_text = enhance_subheadings_with_bold(content_text)
            content_text = remove_duplicate_headings(content_text)
            
            # Add content with proper markdown rendering using grouped approach
            if content_text and content_text.strip():
                render_markdown_to_pdf_grouped(pdf, content_text, primary_color, accent_color, text_color)
            else:
                # Add placeholder text to prevent blank page
                pdf.multi_cell(160, 8, f"Content for {section.title} section is being processed...", align='L')
                
            # Add visual separation between content and charts
            if content_text and content_text.strip():
                pdf.ln(15)  # More generous spacing for better visual separation
            
            # ---- Charts for this section ----
            charts_added = 0  # IMPORTANT: initialize to avoid NameError

            # CRITICAL DEBUG: Show exactly what charts exist and their source_section values
            if section.title == "Financial Plan & Funding":
                # logging.info(f"=== LAST SECTION DEBUG ===")
                # logging.info(f"Section title: '{section.title}'")
                # logging.info(f"All chart_info_map keys: {list(chart_info_map.keys())}")
                for key, charts in chart_info_map.items():
                    # logging.info(f"  Key '{key}' has {len(charts)} charts")
                    for chart in charts:
                        # logging.info(f"    Chart: {chart['title']} ({chart['type']})")
                        pass
                # logging.info(f"=== END LAST SECTION DEBUG ===")
                pass
            
            section_charts = chart_info_map.get(section.title, [])
            # logging.info(f"Section '{section.title}': Direct lookup found {len(section_charts)} charts")
            
            # Strategy 1: Exact title match
            if not section_charts:
                # logging.info(f"No exact match found for '{section.title}'")
                pass
                
                # Strategy 2: Case-insensitive match
                for chart_source, charts in chart_info_map.items():
                    if chart_source.lower() == section.title.lower():
                        section_charts = charts
                        # logging.info(f"Found charts via case-insensitive match: '{chart_source}' -> {len(charts)} charts")
                        break
                
                # Strategy 3: Partial match
                if not section_charts:
                    for chart_source, charts in chart_info_map.items():
                        if (section.title.lower() in chart_source.lower() or 
                            chart_source.lower() in section.title.lower()):
                            section_charts = charts
                            # logging.info(f"Found charts via partial match: '{chart_source}' -> {len(charts)} charts")
                            break
                
                # Strategy 4: Key-based match (for prompt_ prefixed sections)
                if not section_charts and hasattr(section, 'key'):
                    for chart_source, charts in chart_info_map.items():
                        if section.key and section.key.replace('prompt_', '') in chart_source.lower():
                            section_charts = charts
                            # logging.info(f"Found charts via key match: '{section.key}' -> '{chart_source}' -> {len(charts)} charts")
                            break
            
            # logging.info(f"Section '{section.title}': Final chart count: {len(section_charts)}")

            if section_charts:
                
                # Group charts that can fit on the same page
                charts_per_page = 0
                max_charts_per_page = 5  # Allow up to 5 charts per page
                
                for chart_info in section_charts:
                    try:
                        # Check if we need a new page for this chart
                        chart_height_needed = 140  # Chart height (120) + title (20) + minimal spacing
                        
                        # Smart page break logic - allow multiple charts per page
                        current_y = pdf.get_y()
                        charts_per_page += 1
                        
                        # Only force page break if absolutely necessary - allow up to 5 charts per page
                        if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
                            pdf.add_page()
                            # Header/footer removed for now
                            y_title = 25  # Start at normal position
                            charts_per_page = 1  # Reset counter for new page
                        else:
                            y_title = current_y
                        
                        # Add minimal spacing before chart if not at top of page
                        if y_title > 25:  # If not at top of page, add minimal space
                            pdf.ln(3)  # Minimal spacing to fit more charts per page
                        
                        # Chart title is already included in the chart itself, no need for separate subheader

                        # Centered image with larger dimensions for better visibility
                        img_width = 160  # Much larger chart width for better visibility
                        x = (210 - img_width) / 2
                        y_img = y_title

                        # Save image to temp file
                        with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
                            tmp_img.write(chart_info["chart_bytes"])
                            temp_img_path = tmp_img.name

                        # Chart dimensions (no border needed)
                        chart_height = 120  # Much larger height for better visibility
                        chart_width = img_width

                        # Image
                        pdf.image(temp_img_path, x=x, y=y_img, w=chart_width)

                        # Move below image and clean up (spacing based on actual chart height)
                        pdf.ln(chart_height + 5)  # Chart height (120) + minimal spacing between charts
                        try:
                            os.remove(temp_img_path)
                        except Exception:
                            pass

                        charts_added += 1
                        total_charts_placed += 1
                    except Exception:
                        # Skip faulty chart and continue rendering others
                        continue
            else:
                # Debug: Check if there are any charts that might match this section
                potential_matches = []
                for chart_source, charts in chart_info_map.items():
                    if (section.title.lower() in chart_source.lower() or 
                        chart_source.lower() in section.title.lower()):
                        potential_matches.append(f"'{chart_source}' -> {len(charts)} charts")
                
                if potential_matches:
                    # logging.info(f"Section '{section.title}': Potential chart matches: {potential_matches}")
                    pass
                else:
                    # logging.info(f"Section '{section.title}': No charts found and no potential matches")
                    pass
            
            # Each section is already on its own page, no need for complex page break logic
        

    
    
    # ===== BUILD THE PDF =====
    try:
        # Add all sections
        try:
            add_cover_page()
        except Exception as e:
            raise Exception(f"Cover page failed: {str(e)}")
            
        # Add TOC on page 2 with REAL page numbers (already calculated above)
        try:
            if toc_items:
                add_table_of_contents_page(pdf, toc_items, request.businessIdea)
            else:
                # Add a basic TOC page if no items were generated
                pdf.add_page()
                pdf.set_font("Arial", "B", 20)
                pdf.set_text_color(0, 0, 0)
                pdf.set_xy(25, 25)
                pdf.cell(0, 18, "Table of Contents", ln=True)
                pdf.set_font("Arial", "", 12)
                pdf.set_text_color(51, 51, 51)
                pdf.set_xy(25, 50)
                pdf.multi_cell(160, 8, "No sections available for table of contents.", align='L')
        except Exception as e:
            # logging.error(f"Table of contents failed: {str(e)}")
            pass
            # Add a basic TOC page as fallback
            pdf.add_page()
            pdf.set_font("Arial", "B", 20)
            pdf.set_text_color(0, 0, 0)
            pdf.set_xy(25, 25)
            pdf.cell(0, 18, "Table of Contents", ln=True)
            pdf.set_font("Arial", "", 12)
            pdf.set_text_color(51, 51, 51)
            pdf.set_xy(25, 50)
            pdf.multi_cell(160, 8, "Table of contents could not be generated.", align='L')
            
        # Executive summary is now handled as part of the sections
        # No separate page needed - the first section contains the executive summary
            
        try:
            add_sections_with_charts()
        except Exception as e:
            raise Exception(f"Sections with charts failed: {str(e)}")
            
        # CRITICAL CHECK: Ensure we have content in the PDF
        total_pages = pdf.page_no()
        
        if total_pages < 2:  # Should have at least cover + 1 content page
            # Add a content page to prevent blank PDF
            pdf.add_page()
            pdf.set_font("Arial", "B", 16)
            pdf.set_text_color(255, 0, 0)
            pdf.cell(0, 20, "Content Processing Issue", ln=True, align='C')
            pdf.set_font("Arial", "", 12)
            pdf.set_text_color(0, 0, 0)
            pdf.cell(0, 10, "The PDF generation encountered an issue with content processing.", ln=True, align='C')
            pdf.cell(0, 10, "Please check the server logs for detailed information.", ln=True, align='C')
        
        # Get PDF output and convert to bytes
        try:
            pdf_output = pdf.output(dest='S')
            if isinstance(pdf_output, bytearray):
                pdf_bytes = bytes(pdf_output)
            else:
                pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
            
            # CRITICAL SAFETY CHECK: Ensure PDF is not empty
            if len(pdf_bytes) < 1000:  # PDF should be at least 1KB
                # Try to add some content to prevent blank PDF
                pdf.add_page()
                pdf.set_font("Arial", "B", 16)
                pdf.set_text_color(255, 0, 0)  # Red text
                pdf.cell(0, 20, "PDF Generation Issue Detected", ln=True, align='C')
                pdf.set_font("Arial", "", 12)
                pdf.set_text_color(0, 0, 0)
                pdf.cell(0, 10, "If you see this message, there was an issue with content processing.", ln=True, align='C')
                pdf.cell(0, 10, "Please check the server logs for details.", ln=True, align='C')
                
                # Regenerate PDF with error message
                pdf_output = pdf.output(dest='S')
                if isinstance(pdf_output, bytearray):
                    pdf_bytes = bytes(pdf_output)
                else:
                    pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
            
            return pdf_bytes, f"business_plan_{request.businessIdea or 'export'}.pdf"
        except Exception as e:
            raise Exception(f"PDF conversion failed: {str(e)}")
        
    except Exception as e:
        # logging.error(f"PDF generation failed: {e}")
        raise Exception(f"Failed to generate professional PDF: {str(e)}")